Alerting: Recognise & change UI for OnCall notification policy + contact point (#60259)

* Identify and show onCall contact points with a badge in case the plugin is installed

* Add onCall logo for onCall contact points Badge

* Refactor and make Grafana App Receiver type more generic, not only for onCall type

* Show onCall notification policy in the specific routing table with special onCall badge

* Fix tests

* Move onCall badge to the type column in contact points view table

* Fix typos and remove onCallIntegrations from tagTypes in alertingApi

* Fetch only local plugins instead of all (external are not needed) and don't fetch plugin details

* Use directly useGetOnCallIntegrationsQuery and more PR review suggestions

* Move onCall contact point to the top in the drop-down,  in the notification policy view

* Add PR review requested changes
This commit is contained in:
Sonia Aguilar 2022-12-21 14:46:55 +01:00 committed by GitHub
parent c537d3699c
commit e219e2a834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 282 additions and 40 deletions

View File

@ -2799,9 +2799,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/AmRoutes.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/alerting/unified/AmRoutes.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/PanelAlertTabContent.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { useCleanup } from '../../../core/hooks/useCleanup';
@ -17,12 +16,14 @@ import { ProvisionedResource, ProvisioningAlert } from './components/Provisionin
import { AmRootRoute } from './components/amroutes/AmRootRoute';
import { AmSpecificRouting } from './components/amroutes/AmSpecificRouting';
import { MuteTimingsTable } from './components/amroutes/MuteTimingsTable';
import { useGetAmRouteReceiverWithGrafanaAppTypes } from './components/receivers/grafanaAppReceivers/grafanaApp';
import { AmRouteReceiver } from './components/receivers/grafanaAppReceivers/types';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction, updateAlertManagerConfigAction } from './state/actions';
import { AmRouteReceiver, FormAmRoute } from './types/amroutes';
import { amRouteToFormAmRoute, formAmRouteToAmRoute, stringsToSelectableValues } from './utils/amroutes';
import { FormAmRoute } from './types/amroutes';
import { amRouteToFormAmRoute, formAmRouteToAmRoute } from './utils/amroutes';
import { isVanillaPrometheusAlertManagerDataSource } from './utils/datasource';
import { initialAsyncRequestState } from './utils/redux';
@ -56,9 +57,7 @@ const AmRoutes = () => {
const config = result?.alertmanager_config;
const [rootRoute, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
const receivers = stringsToSelectableValues(
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
) as AmRouteReceiver[];
const receivers: AmRouteReceiver[] = useGetAmRouteReceiverWithGrafanaAppTypes(config?.receivers ?? []);
const isProvisioned = useMemo(() => Boolean(config?.route?.provenance), [config?.route]);

View File

@ -27,6 +27,7 @@ import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManager
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { fetchNotifiers } from './api/grafana';
import * as receiversApi from './api/receiversApi';
import * as grafanaApp from './components/receivers/grafanaAppReceivers/grafanaApp';
import {
mockDataSource,
MockDataSourceSrv,
@ -143,6 +144,8 @@ const clickSelectOption = async (selectElement: HTMLElement, optionText: string)
document.addEventListener('click', interceptLinkClicks);
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker');
describe('Receivers', () => {
const server = setupServer();
@ -158,6 +161,7 @@ describe('Receivers', () => {
beforeEach(() => {
server.resetHandlers();
jest.resetAllMocks();
useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined);
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });

View File

@ -5,7 +5,7 @@ import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
import { logInfo } from '../Analytics';
const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (requestOptions) => {
export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (requestOptions) => {
try {
const requestStartTs = performance.now();

View File

@ -0,0 +1,20 @@
import { alertingApi } from './alertingApi';
export interface OnCallIntegration {
integration_url: string;
}
export type OnCallIntegrationsResponse = OnCallIntegration[];
export type OnCallIntegrationsUrls = string[];
export const onCallApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
getOnCallIntegrations: build.query<OnCallIntegrationsUrls, void>({
query: () => ({
headers: {},
url: '/api/plugin-proxy/grafana-oncall-app/api/internal/v1/alert_receive_channels/',
}),
providesTags: ['AlertmanagerChoice'],
transformResponse: (response: OnCallIntegrationsResponse) => response.map((result) => result.integration_url),
}),
}),
});
export const { useGetOnCallIntegrationsQuery } = onCallApi;

View File

@ -5,8 +5,9 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui';
import { Authorize } from '../../components/Authorize';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import { getNotificationsPermissions } from '../../utils/access-control';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
import { AmRootRouteForm } from './AmRootRouteForm';
import { AmRootRouteRead } from './AmRootRouteRead';

View File

@ -3,7 +3,7 @@ import React, { FC, useState } from 'react';
import { Button, Collapse, Field, Form, Input, InputControl, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import {
mapMultiSelectValueToStrings,
mapSelectValueToString,
@ -14,6 +14,7 @@ import {
} from '../../utils/amroutes';
import { makeAMLink } from '../../utils/misc';
import { timeOptions } from '../../utils/time';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
import { getFormStyles } from './formStyles';

View File

@ -20,7 +20,7 @@ import {
} from '@grafana/ui';
import { useMuteTimingOptions } from '../../hooks/useMuteTimingOptions';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import { matcherFieldOptions } from '../../utils/alertmanager';
import {
emptyArrayFieldMatcher,
@ -32,6 +32,7 @@ import {
commonGroupByOptions,
} from '../../utils/amroutes';
import { timeOptions } from '../../utils/time';
import { AmRouteReceiver, GrafanaAppReceiverEnum } from '../receivers/grafanaAppReceivers/types';
import { getFormStyles } from './formStyles';
@ -48,6 +49,14 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(routes.groupBy));
const muteTimingOptions = useMuteTimingOptions();
const receiversWithOnCallOnTop = receivers.sort((receiver1, receiver2) => {
if (receiver1.grafanaAppReceiverType === GrafanaAppReceiverEnum.GRAFANA_ONCALL) {
return -1;
} else {
return 0;
}
});
return (
<Form defaultValues={routes} onSubmit={onSave}>
{({ control, register, errors, setValue, watch }) => (
@ -148,7 +157,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receivers}
options={receiversWithOnCallOnTop}
/>
)}
control={control}

View File

@ -4,10 +4,11 @@ import React, { FC, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import { getNotificationsPermissions } from '../../utils/access-control';
import { emptyRoute } from '../../utils/amroutes';
import { Authorize } from '../Authorize';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
import { AmRoutesTable } from './AmRoutesTable';
import { MuteTimingsTable } from './MuteTimingsTable';

View File

@ -4,12 +4,14 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import { getNotificationsPermissions } from '../../utils/access-control';
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
import { prepareItems } from '../../utils/dynamicTable';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { EmptyArea } from '../EmptyArea';
import { GrafanaAppBadge } from '../receivers/grafanaAppReceivers/GrafanaAppBadge';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
import { Matchers } from '../silences/Matchers';
import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
@ -67,6 +69,10 @@ export const deleteRoute = (routes: FormAmRoute[], routeId: string): FormAmRoute
return routes.filter((route) => route.id !== routeId);
};
export const getGrafanaAppReceiverType = (receivers: AmRouteReceiver[], receiverName: string) => {
return receivers.find((receiver) => receiver.label === receiverName)?.grafanaAppReceiverType;
};
export const AmRoutesTable: FC<AmRoutesTableProps> = ({
isAddMode,
onCancelAdd,
@ -112,7 +118,16 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
{
id: 'receiverChannel',
label: 'Contact point',
renderCell: (item) => item.data.receiver || '-',
renderCell: (item) => {
const type = getGrafanaAppReceiverType(receivers, item.data.receiver);
return item.data.receiver ? (
<>
{item.data.receiver} {type && <GrafanaAppBadge grafanaAppType={type} />}
</>
) : (
'-'
);
},
size: 5,
},
{

View File

@ -8,13 +8,14 @@ import { contextSrv } from 'app/core/services/context_srv';
import { Authorize } from '../../components/Authorize';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { FormAmRoute } from '../../types/amroutes';
import { getNotificationsPermissions } from '../../utils/access-control';
import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
import { getNotificationPoliciesFilters } from '../../utils/misc';
import { EmptyArea } from '../EmptyArea';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
import { MatcherFilter } from '../alert-groups/MatcherFilter';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
import { AmRoutesTable } from './AmRoutesTable';

View File

@ -12,10 +12,12 @@ import {
import { configureStore } from 'app/store/configureStore';
import { ContactPointsState, NotifierDTO, NotifierType } from 'app/types';
import * as onCallApi from '../../api/onCallApi';
import * as receiversApi from '../../api/receiversApi';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { ReceiversTable } from './ReceiversTable';
import * as grafanaApp from './grafanaAppReceivers/grafanaApp';
const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierDTO[]) => {
const config: AlertManagerCortexConfig = {
@ -53,6 +55,8 @@ const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({
options: [],
});
jest.spyOn(onCallApi, 'useGetOnCallIntegrationsQuery');
const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker');
const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPointsState');
describe('ReceiversTable', () => {
@ -60,6 +64,7 @@ describe('ReceiversTable', () => {
jest.resetAllMocks();
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState);
useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined);
});
it('render receivers with grafana notifiers', async () => {

View File

@ -6,7 +6,7 @@ import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Button, ConfirmModal, Modal, useStyles2, Badge, Icon } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertManagerCortexConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch, AccessControlAction, ContactPointsState, NotifiersState, ReceiversState } from 'app/types';
import { useGetContactPointsState } from '../../api/receiversApi';
@ -24,6 +24,9 @@ import { ProvisioningBadge } from '../Provisioning';
import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection';
import { GrafanaAppBadge } from './grafanaAppReceivers/GrafanaAppBadge';
import { useGetReceiversWithGrafanaAppTypes } from './grafanaAppReceivers/grafanaApp';
import { GrafanaAppReceiverEnum, ReceiverWithTypes } from './grafanaAppReceivers/types';
interface UpdateActionProps extends ActionProps {
onClickDeleteReceiver: (receiverName: string) => void;
@ -128,6 +131,7 @@ interface ReceiverItem {
name: string;
types: string[];
provisioned?: boolean;
grafanaAppReceiverType?: GrafanaAppReceiverEnum;
}
interface NotifierStatus {
@ -261,10 +265,10 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
}
setReceiverToDelete(undefined);
};
const rows: RowItemTableProps[] = useMemo(
() =>
config.alertmanager_config.receivers?.map((receiver: Receiver) => ({
const receivers = useGetReceiversWithGrafanaAppTypes(config.alertmanager_config.receivers ?? []);
const rows: RowItemTableProps[] = useMemo(() => {
return (
receivers?.map((receiver: ReceiverWithTypes) => ({
id: receiver.name,
data: {
name: receiver.name,
@ -276,11 +280,13 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
return type;
}
),
grafanaAppReceiverType: receiver.grafanaAppReceiverType,
provisioned: receiver.grafana_managed_receiver_configs?.some((receiver) => receiver.provenance),
},
})) ?? [],
[config, grafanaNotifiers.result]
})) ?? []
);
}, [grafanaNotifiers.result, receivers]);
const columns = useGetColumns(
alertManagerName,
errorStateAvailable,
@ -296,7 +302,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
title="Contact points"
description="Define where the notifications will be sent to, for example email or Slack."
showButton={!isVanillaAM && contextSrv.hasPermission(permissions.create)}
addButtonLabel="New contact point"
addButtonLabel={'New contact point'}
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
>
<DynamicTable
@ -370,16 +376,19 @@ function useGetColumns(
id: 'name',
label: 'Contact point name',
renderCell: ({ data: { name, provisioned } }) => (
<>
{name} {provisioned && <ProvisioningBadge />}
</>
<Stack alignItems="center">
<div>{name}</div>
{provisioned && <ProvisioningBadge />}
</Stack>
),
size: 1,
},
{
id: 'type',
label: 'Type',
renderCell: ({ data: { types } }) => <>{types.join(', ')}</>,
renderCell: ({ data: { types, grafanaAppReceiverType } }) => (
<>{grafanaAppReceiverType ? <GrafanaAppBadge grafanaAppType={grafanaAppReceiverType} /> : types.join(', ')}</>
),
size: 1,
},
];
@ -435,4 +444,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: ${theme.colors.warning.text};
`,
countMessage: css``,
onCallBadgeWrapper: css`
text-align: left;
height: 22px;
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
border: 1px solid rgba(245, 95, 62, 1);
color: rgba(245, 95, 62, 1);
font-weight: ${theme.typography.fontWeightRegular};
`,
});

View File

@ -0,0 +1,32 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup, useStyles2 } from '@grafana/ui';
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE, GrafanaAppReceiverEnum } from './types';
export const GrafanaAppBadge = ({ grafanaAppType }: { grafanaAppType: GrafanaAppReceiverEnum }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.wrapper}>
<HorizontalGroup align="center" spacing="xs">
<img src={GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[grafanaAppType]} alt="" height="12px" />
<span>{grafanaAppType}</span>
</HorizontalGroup>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
text-align: left;
height: 22px;
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
border: 1px solid rgba(245, 95, 62, 1);
color: rgba(245, 95, 62, 1);
font-weight: ${theme.typography.fontWeightRegular};
`,
});

View File

@ -0,0 +1,56 @@
import { useGetSingleLocalWithoutDetails } from 'app/features/plugins/admin/state/hooks';
import { CatalogPlugin } from 'app/features/plugins/admin/types';
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
import { useGetOnCallIntegrationsQuery } from '../../../api/onCallApi';
import { isOnCallReceiver } from './onCall/onCall';
import { AmRouteReceiver, GrafanaAppReceiverEnum, GRAFANA_APP_PLUGIN_IDS, ReceiverWithTypes } from './types';
export const useGetAppIsInstalledAndEnabled = (grafanaAppType: GrafanaAppReceiverEnum) => {
// fetches the plugin settings for this Grafana instance
const plugin: CatalogPlugin | undefined = useGetSingleLocalWithoutDetails(GRAFANA_APP_PLUGIN_IDS[grafanaAppType]);
return plugin?.isInstalled && !plugin?.isDisabled && plugin?.type === 'app';
};
export const useGetGrafanaReceiverTypeChecker = () => {
const isOnCallEnabled = useGetAppIsInstalledAndEnabled(GrafanaAppReceiverEnum.GRAFANA_ONCALL);
const { data } = useGetOnCallIntegrationsQuery(undefined, {
skip: !isOnCallEnabled,
});
const getGrafanaReceiverType = (receiver: Receiver): GrafanaAppReceiverEnum | undefined => {
//CHECK FOR ONCALL PLUGIN
const onCallIntegrations = data ?? [];
if (isOnCallEnabled && isOnCallReceiver(receiver, onCallIntegrations)) {
return GrafanaAppReceiverEnum.GRAFANA_ONCALL;
}
//WE WILL ADD IN HERE IF THERE ARE MORE TYPES TO CHECK
return undefined;
};
return getGrafanaReceiverType;
};
export const useGetAmRouteReceiverWithGrafanaAppTypes = (receivers: Receiver[]) => {
const getGrafanaReceiverType = useGetGrafanaReceiverTypeChecker();
const receiverToSelectableContactPointValue = (receiver: Receiver): AmRouteReceiver => {
const amRouteReceiverValue: AmRouteReceiver = {
label: receiver.name,
value: receiver.name,
grafanaAppReceiverType: getGrafanaReceiverType(receiver),
};
return amRouteReceiverValue;
};
return receivers.map(receiverToSelectableContactPointValue);
};
export const useGetReceiversWithGrafanaAppTypes = (receivers: Receiver[]): ReceiverWithTypes[] => {
const getGrafanaReceiverType = useGetGrafanaReceiverTypeChecker();
return receivers.map((receiver: Receiver) => {
return {
...receiver,
grafanaAppReceiverType: getGrafanaReceiverType(receiver),
};
});
};

View File

@ -0,0 +1,19 @@
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
export const isInOnCallIntegrations = (url: string, integrationsUrls: string[]) => {
return integrationsUrls.includes(url);
};
export const isOnCallReceiver = (receiver: Receiver, integrationsUrls: string[]) => {
if (!receiver.grafana_managed_receiver_configs) {
return false;
}
// A receiver it's an onCall contact point if it includes only one integration, and this integration it's an onCall
// An integration it's an onCall type if it's included in the list of integrations returned by the onCall api endpoint
const onlyOneIntegration = receiver.grafana_managed_receiver_configs.length === 1;
const isOncall = isInOnCallIntegrations(
receiver.grafana_managed_receiver_configs[0]?.settings?.url ?? '',
integrationsUrls
);
return onlyOneIntegration && isOncall;
};

View File

@ -0,0 +1,23 @@
import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
// we will add in here more types if needed
export enum GrafanaAppReceiverEnum {
GRAFANA_ONCALL = 'Grafana OnCall',
}
export interface AmRouteReceiver {
label: string;
value: string;
grafanaAppReceiverType?: GrafanaAppReceiverEnum;
}
export interface ReceiverWithTypes extends Receiver {
grafanaAppReceiverType?: GrafanaAppReceiverEnum;
}
export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE = {
'Grafana OnCall': 'public/img/alerting/oncall_logo.svg',
};
export enum GRAFANA_APP_PLUGIN_IDS {
'Grafana OnCall' = 'grafana-oncall-app',
}

View File

@ -17,8 +17,3 @@ export interface FormAmRoute {
muteTimeIntervals: string[];
routes: FormAmRoute[];
}
export interface AmRouteReceiver {
label: string;
value: string;
}

View File

@ -15,8 +15,8 @@ import {
uninstallPlugin,
} from '../api';
import { STATE_PREFIX } from '../constants';
import { mergeLocalsAndRemotes, updatePanels } from '../helpers';
import { CatalogPlugin, RemotePlugin } from '../types';
import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers';
import { CatalogPlugin, RemotePlugin, LocalPlugin } from '../types';
export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => {
try {
@ -33,6 +33,15 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t
}
});
export const fetchAllLocal = createAsyncThunk(`${STATE_PREFIX}/fetchAllLocal`, async (_, thunkApi) => {
try {
const localPlugins = await getLocalPlugins();
return localPlugins.map((plugin: LocalPlugin) => mapLocalToCatalog(plugin));
} catch (e) {
return thunkApi.rejectWithValue('Unknown error.');
}
});
export const fetchRemotePlugins = createAsyncThunk<RemotePlugin[], void, { rejectValue: RemotePlugin[] }>(
`${STATE_PREFIX}/fetchRemotePlugins`,
async (_, thunkApi) => {

View File

@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'app/types';
import { sortPlugins, Sorters } from '../helpers';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall } from './actions';
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal } from './actions';
import { setDisplayMode } from './reducer';
import {
find,
@ -58,6 +58,11 @@ export const useGetSingle = (id: string): CatalogPlugin | undefined => {
return useSelector((state) => selectById(state, id));
};
export const useGetSingleLocalWithoutDetails = (id: string): CatalogPlugin | undefined => {
useFetchAllLocal();
return useSelector((state) => selectById(state, id));
};
export const useGetErrors = (): PluginError[] => {
useFetchAll();
@ -118,6 +123,16 @@ export const useFetchAll = () => {
}, []); // eslint-disable-line
};
// Only fetches in case they were not fetched yet
export const useFetchAllLocal = () => {
const dispatch = useDispatch();
const isNotFetched = useSelector(selectIsRequestNotFetched(fetchAllLocal.typePrefix));
useEffect(() => {
isNotFetched && dispatch(fetchAllLocal());
}, []); // eslint-disable-line
};
export const useFetchDetails = (id: string) => {
const dispatch = useDispatch();
const plugin = useSelector((state) => selectById(state, id));

View File

@ -5,7 +5,15 @@ import { PanelPlugin } from '@grafana/data';
import { STATE_PREFIX } from '../constants';
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
import { fetchAll, fetchDetails, install, uninstall, loadPluginDashboards, panelPluginLoaded } from './actions';
import {
fetchAll,
fetchDetails,
install,
uninstall,
loadPluginDashboards,
panelPluginLoaded,
fetchAllLocal,
} from './actions';
export const pluginsAdapter = createEntityAdapter<CatalogPlugin>();
@ -54,6 +62,10 @@ const slice = createSlice({
.addCase(fetchAll.fulfilled, (state, action) => {
pluginsAdapter.upsertMany(state.items, action.payload);
})
// Fetch All local
.addCase(fetchAllLocal.fulfilled, (state, action) => {
pluginsAdapter.upsertMany(state.items, action.payload);
})
// Fetch Details
.addCase(fetchDetails.fulfilled, (state, action) => {
pluginsAdapter.updateOne(state.items, action.payload);

View File

@ -0,0 +1,9 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.3009 29.8017H30.6991C32.0765 29.8017 32.9322 28.313 32.2435 27.1235L25.5443 15.52C24.8557 14.3304 23.1374 14.3304 22.4557 15.52L15.7565 27.1235C15.0678 28.313 15.9304 29.8017 17.3009 29.8017ZM24 0C10.7478 0 0 10.7478 0 24C0 37.2522 10.7478 48 24 48C37.2522 48 48 37.2522 48 24C48 10.7478 37.2522 0 24 0ZM24.0626 10.9774C31.2557 11.0122 37.0574 16.8696 37.0226 24.0626C36.9878 31.2557 31.1304 37.0574 23.9374 37.0226C16.7443 36.9878 10.9426 31.1304 10.9774 23.9374C11.0122 16.7443 16.8696 10.9426 24.0626 10.9774ZM6.94261 31.3809C6.79652 31.4296 6.65739 31.4504 6.5113 31.4504C5.92696 31.4504 5.37739 31.0748 5.18957 30.4904C4.53565 28.48 4.2087 26.3165 4.2087 24.0626C4.2087 22.8104 4.31304 21.5026 4.52174 20.1878C6.09391 12.2017 12.2296 6.03826 20.16 4.4313C20.9183 4.27826 21.6487 4.76522 21.8017 5.51652C21.9548 6.26783 21.4678 7.00522 20.7165 7.15826C13.8922 8.53565 8.61217 13.8435 7.26957 20.6678C7.09565 21.7878 7.00522 22.9496 7.00522 24.0557C7.00522 26.0104 7.29043 27.8887 7.85391 29.6209C8.09044 30.3513 7.69391 31.1374 6.95652 31.3739L6.94261 31.3809ZM39.1791 37.127C35.52 41.4817 30.2539 43.8748 24.3548 43.8748C18.4557 43.8748 12.8626 41.447 9.05739 37.2174C8.54261 36.647 8.5913 35.7635 9.16174 35.2557C9.73217 34.7409 10.6157 34.7896 11.1235 35.36C14.3513 38.9496 19.2904 41.0991 24.3478 41.0991C29.4052 41.0991 33.92 39.0539 37.0435 35.3391C37.5374 34.7548 38.4139 34.6713 39.0052 35.1722C39.5965 35.6661 39.6661 36.5426 39.1722 37.1339L39.1791 37.127ZM42.9426 30.4C42.7409 30.9774 42.2052 31.3322 41.6278 31.3322C41.4748 31.3322 41.3217 31.3043 41.1687 31.2557C40.4452 31.0052 40.0626 30.2122 40.313 29.4817C40.5565 28.7722 40.7791 28.0278 40.96 27.2626C41.1687 26.2122 41.28 25.0852 41.28 23.9722C41.28 15.8052 35.4574 8.73044 27.4296 7.14435C26.6783 6.99826 26.1565 6.26783 26.2957 5.51652C26.4348 4.76522 27.1304 4.26435 27.8748 4.39652C27.8887 4.39652 27.9513 4.41043 27.9652 4.41739C37.287 6.25391 44.0557 14.4835 44.0557 23.9722C44.0557 25.2661 43.9304 26.5739 43.673 27.8539C43.4574 28.7652 43.2139 29.6 42.9357 30.4H42.9426Z" fill="url(#paint0_linear_911_12416)"/>
<defs>
<linearGradient id="paint0_linear_911_12416" x1="24.3556" y1="47.7468" x2="24.3556" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#FAC10D"/>
<stop offset="1" stop-color="#F05A28"/>
</linearGradient>
</defs>
<script xmlns=""/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB