Compare commits
1 Commits
main
...
alerting/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8595844334 |
@@ -1337,11 +1337,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/api/onCallApi.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -1627,11 +1622,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/configure.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/handlers/plugins.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -1662,16 +1652,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/datasource.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getIrmIfPresentOrIncidentPluginId } from '../utils/config';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
interface IncidentsPluginConfigDto {
|
||||
@@ -7,13 +5,13 @@ interface IncidentsPluginConfigDto {
|
||||
isIncidentCreated: boolean;
|
||||
}
|
||||
|
||||
const getProxyApiUrl = (path: string) => `/api/plugins/${getIrmIfPresentOrIncidentPluginId()}/resources${path}`;
|
||||
const getProxyApiUrl = (path: string, pluginId: string) => `/api/plugins/${pluginId}/resources${path}`;
|
||||
|
||||
export const incidentsApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker'),
|
||||
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker', pluginId),
|
||||
data: {},
|
||||
method: 'POST',
|
||||
showErrorAlert: false,
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { getProxyApiUrl } from './onCallApi';
|
||||
|
||||
describe('getProxyApiUrl', () => {
|
||||
it('should return URL with IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
|
||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||
it('should return URL with IRM plugin ID when IRM plugin ID is passed', () => {
|
||||
expect(getProxyApiUrl('/alert_receive_channels/', SupportedPlugin.Irm)).toBe(
|
||||
'/api/plugins/grafana-irm-app/resources/alert_receive_channels/'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return URL with OnCall plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
|
||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||
it('should return URL with OnCall plugin ID when OnCall plugin ID is passed', () => {
|
||||
expect(getProxyApiUrl('/alert_receive_channels/', SupportedPlugin.OnCall)).toBe(
|
||||
'/api/plugins/grafana-oncall-app/resources/alert_receive_channels/'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FetchError, isFetchError } from '@grafana/runtime';
|
||||
|
||||
import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../utils/config';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
@@ -38,15 +37,15 @@ export interface OnCallConfigChecks {
|
||||
is_integration_chatops_connected: boolean;
|
||||
}
|
||||
|
||||
export function getProxyApiUrl(path: string) {
|
||||
return `/api/plugins/${getIrmIfPresentOrOnCallPluginId()}/resources${path}`;
|
||||
export function getProxyApiUrl(path: string, pluginId: string) {
|
||||
return `/api/plugins/${pluginId}/resources${path}`;
|
||||
}
|
||||
|
||||
export const onCallApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
grafanaOnCallIntegrations: build.query<OnCallIntegrationDTO[], void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/'),
|
||||
grafanaOnCallIntegrations: build.query<OnCallIntegrationDTO[], { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/', pluginId),
|
||||
// legacy_grafana_alerting is necessary for OnCall.
|
||||
// We do NOT need to differentiate between these two on our side
|
||||
params: {
|
||||
@@ -64,31 +63,31 @@ export const onCallApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
providesTags: ['OnCallIntegrations'],
|
||||
}),
|
||||
validateIntegrationName: build.query<boolean, string>({
|
||||
query: (name) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/validate_name/'),
|
||||
validateIntegrationName: build.query<boolean, { name: string; pluginId: string }>({
|
||||
query: ({ name, pluginId }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/validate_name/', pluginId),
|
||||
params: { verbal_name: name },
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO>({
|
||||
query: (integration) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/'),
|
||||
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO & { pluginId: string }>({
|
||||
query: ({ pluginId, ...integration }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/', pluginId),
|
||||
data: integration,
|
||||
method: 'POST',
|
||||
showErrorAlert: true,
|
||||
}),
|
||||
invalidatesTags: ['OnCallIntegrations'],
|
||||
}),
|
||||
features: build.query<OnCallFeature[], void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/features/'),
|
||||
features: build.query<OnCallFeature[], { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/features/', pluginId),
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
onCallConfigChecks: build.query<OnCallConfigChecks, void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/organization/config-checks/'),
|
||||
onCallConfigChecks: build.query<OnCallConfigChecks, { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/organization/config-checks/', pluginId),
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config';
|
||||
import { useIrmPlugin } from '../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import { createBridgeURL } from '../PluginBridge';
|
||||
|
||||
interface Props {
|
||||
@@ -11,20 +11,18 @@ interface Props {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const pluginId = getIrmIfPresentOrIncidentPluginId();
|
||||
|
||||
export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => {
|
||||
const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.Incident);
|
||||
|
||||
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
|
||||
title,
|
||||
severity,
|
||||
url,
|
||||
});
|
||||
|
||||
const { loading, installed, settings } = usePluginBridge(pluginId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading === true && (
|
||||
{loading && (
|
||||
<Button icon="fire" size="sm" type="button" disabled>
|
||||
<Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
|
||||
</Button>
|
||||
@@ -51,17 +49,17 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P
|
||||
};
|
||||
|
||||
export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => {
|
||||
const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.Incident);
|
||||
|
||||
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
|
||||
title,
|
||||
severity,
|
||||
url,
|
||||
});
|
||||
|
||||
const { loading, installed, settings } = usePluginBridge(pluginId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading === true && (
|
||||
{loading && (
|
||||
<Menu.Item
|
||||
label={t('alerting.declare-incident-menu-item.label-declare-incident', 'Declare incident')}
|
||||
icon="fire"
|
||||
|
||||
@@ -18,10 +18,10 @@ import { getAPINamespace } from '../../../../../api/utils';
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { onCallApi } from '../../api/onCallApi';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useIrmPlugin } from '../../hooks/usePluginBridge';
|
||||
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
|
||||
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
|
||||
@@ -61,8 +61,8 @@ const defaultOptions = {
|
||||
* Otherwise, returns no data
|
||||
*/
|
||||
const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
|
||||
const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
|
||||
const { pluginId, installed, loading } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery({ pluginId }, { skip: skip || !installed });
|
||||
|
||||
return useMemo(() => {
|
||||
if (installed) {
|
||||
@@ -126,6 +126,10 @@ export const useGrafanaContactPoints = ({
|
||||
}: GrafanaFetchOptions & Skippable = {}) => {
|
||||
const namespace = getAPINamespace();
|
||||
const potentiallySkip = { skip };
|
||||
|
||||
// Get the IRM/OnCall plugin information
|
||||
const irmOrOnCallPlugin = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const onCallResponse = useOnCallIntegrations(potentiallySkip);
|
||||
const alertNotifiers = useGrafanaNotifiersQuery(undefined, potentiallySkip);
|
||||
const contactPointsListResponse = useK8sContactPoints({ namespace }, potentiallySkip);
|
||||
@@ -158,6 +162,7 @@ export const useGrafanaContactPoints = ({
|
||||
status: contactPointsStatusResponse.data,
|
||||
notifiers: alertNotifiers.data,
|
||||
onCallIntegrations: onCallResponse?.data,
|
||||
onCallPluginId: irmOrOnCallPlugin.pluginId,
|
||||
contactPoints: contactPointsListResponse.data || [],
|
||||
alertmanagerConfiguration: alertmanagerConfigResponse.data,
|
||||
});
|
||||
@@ -172,6 +177,7 @@ export const useGrafanaContactPoints = ({
|
||||
contactPointsListResponse,
|
||||
contactPointsStatusResponse,
|
||||
onCallResponse,
|
||||
irmOrOnCallPlugin.pluginId,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { OnCallIntegrationDTO } from '../../api/onCallApi';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import { extractReceivers } from '../../utils/receivers';
|
||||
import { routeAdapter } from '../../utils/routeAdapter';
|
||||
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
|
||||
@@ -113,7 +114,8 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||
type EnhanceContactPointsArgs = {
|
||||
status?: ReceiversStateDTO[];
|
||||
notifiers?: NotifierDTO[];
|
||||
onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null;
|
||||
onCallIntegrations?: OnCallIntegrationDTO[];
|
||||
onCallPluginId?: SupportedPlugin;
|
||||
contactPoints: Receiver[];
|
||||
alertmanagerConfiguration?: AlertManagerCortexConfig;
|
||||
};
|
||||
@@ -130,6 +132,7 @@ export function enhanceContactPointsWithMetadata({
|
||||
status = [],
|
||||
notifiers = [],
|
||||
onCallIntegrations,
|
||||
onCallPluginId,
|
||||
contactPoints,
|
||||
alertmanagerConfiguration,
|
||||
}: EnhanceContactPointsArgs): ContactPointWithMetadata[] {
|
||||
@@ -162,7 +165,7 @@ export function enhanceContactPointsWithMetadata({
|
||||
[RECEIVER_META_KEY]: getNotifierMetadata(notifiers, receiver),
|
||||
// if OnCall plugin is installed, we'll add it to the receiver's plugin metadata
|
||||
[RECEIVER_PLUGIN_META_KEY]: isOnCallReceiver
|
||||
? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration))
|
||||
? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration), onCallPluginId)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -6,12 +6,12 @@ import { t } from '@grafana/i18n';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { NotifierDTO } from 'app/features/alerting/unified/types/alerting';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
|
||||
import { useAppNotification } from '../../../../../../../core/copy/appNotification';
|
||||
import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { ONCALL_INTEGRATION_V2_FEATURE, onCallApi } from '../../../../api/onCallApi';
|
||||
import { usePluginBridge } from '../../../../hooks/usePluginBridge';
|
||||
import { useIrmPlugin } from '../../../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../../../types/pluginBridges';
|
||||
import { option } from '../../../../utils/notifier-types';
|
||||
|
||||
import { GRAFANA_ONCALL_INTEGRATION_TYPE, ReceiverTypes } from './onCall';
|
||||
@@ -39,16 +39,17 @@ enum OnCallIntegrationStatus {
|
||||
|
||||
function useOnCallPluginStatus() {
|
||||
const {
|
||||
pluginId,
|
||||
installed: isOnCallEnabled,
|
||||
loading: isPluginBridgeLoading,
|
||||
error: pluginError,
|
||||
} = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
} = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const {
|
||||
data: onCallFeatures = [],
|
||||
error: onCallFeaturesError,
|
||||
isLoading: isOnCallFeaturesLoading,
|
||||
} = onCallApi.endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled });
|
||||
} = onCallApi.endpoints.features.useQuery({ pluginId }, { skip: !isOnCallEnabled });
|
||||
|
||||
const integrationStatus = useMemo((): OnCallIntegrationStatus => {
|
||||
if (!isOnCallEnabled) {
|
||||
@@ -67,6 +68,7 @@ function useOnCallPluginStatus() {
|
||||
);
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
isOnCallEnabled,
|
||||
integrationStatus,
|
||||
isAlertingV2IntegrationEnabled,
|
||||
@@ -78,8 +80,14 @@ function useOnCallPluginStatus() {
|
||||
export function useOnCallIntegration() {
|
||||
const notifyApp = useAppNotification();
|
||||
|
||||
const { isOnCallEnabled, integrationStatus, isAlertingV2IntegrationEnabled, isOnCallStatusLoading, onCallError } =
|
||||
useOnCallPluginStatus();
|
||||
const {
|
||||
pluginId,
|
||||
isOnCallEnabled,
|
||||
integrationStatus,
|
||||
isAlertingV2IntegrationEnabled,
|
||||
isOnCallStatusLoading,
|
||||
onCallError,
|
||||
} = useOnCallPluginStatus();
|
||||
|
||||
const { useCreateIntegrationMutation, useGrafanaOnCallIntegrationsQuery, useLazyValidateIntegrationNameQuery } =
|
||||
onCallApi;
|
||||
@@ -91,13 +99,13 @@ export function useOnCallIntegration() {
|
||||
data: grafanaOnCallIntegrations = [],
|
||||
isLoading: isLoadingOnCallIntegrations,
|
||||
isError: isIntegrationsQueryError,
|
||||
} = useGrafanaOnCallIntegrationsQuery(undefined, { skip: !isAlertingV2IntegrationEnabled });
|
||||
} = useGrafanaOnCallIntegrationsQuery({ pluginId }, { skip: !isAlertingV2IntegrationEnabled });
|
||||
|
||||
const onCallFormValidators = useMemo(() => {
|
||||
return {
|
||||
integration_name: async (value: string) => {
|
||||
try {
|
||||
await validateIntegrationNameQuery(value).unwrap();
|
||||
await validateIntegrationNameQuery({ name: value, pluginId }).unwrap();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isFetchError(error) && error.status === 409) {
|
||||
@@ -126,7 +134,7 @@ export function useOnCallIntegration() {
|
||||
: t('alerting.irm-integration.integration-required', 'Selection of existing IRM integration is required');
|
||||
},
|
||||
};
|
||||
}, [grafanaOnCallIntegrations, validateIntegrationNameQuery, isAlertingV2IntegrationEnabled, notifyApp]);
|
||||
}, [grafanaOnCallIntegrations, validateIntegrationNameQuery, isAlertingV2IntegrationEnabled, notifyApp, pluginId]);
|
||||
|
||||
const extendOnCallReceivers = useCallback(
|
||||
(receiver: Receiver): Receiver => {
|
||||
@@ -159,6 +167,7 @@ export function useOnCallIntegration() {
|
||||
|
||||
const createNewOnCallIntegrationJobs = newOnCallIntegrations.map(async (c) => {
|
||||
const newIntegration = await createIntegrationMutation({
|
||||
pluginId,
|
||||
integration: GRAFANA_ONCALL_INTEGRATION_TYPE,
|
||||
verbal_name: c.settings[OnCallIntegrationSetting.IntegrationName],
|
||||
}).unwrap();
|
||||
@@ -180,7 +189,7 @@ export function useOnCallIntegration() {
|
||||
});
|
||||
});
|
||||
},
|
||||
[isAlertingV2IntegrationEnabled, createIntegrationMutation]
|
||||
[isAlertingV2IntegrationEnabled, createIntegrationMutation, pluginId]
|
||||
);
|
||||
|
||||
const extendOnCallNotifierFeatures = useCallback(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { OnCallIntegrationDTO } from '../../../api/onCallApi';
|
||||
import { getIrmIfPresentOrOnCallPluginId, getIsIrmPluginPresent } from '../../../utils/config';
|
||||
import { useIrmPlugin } from '../../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../../types/pluginBridges';
|
||||
import { createBridgeURL } from '../../PluginBridge';
|
||||
|
||||
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types';
|
||||
@@ -13,20 +16,34 @@ export interface ReceiverPluginMetadata {
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()];
|
||||
const onCallReceiverTitle = 'Grafana OnCall';
|
||||
|
||||
export const onCallReceiverMeta: ReceiverPluginMetadata = {
|
||||
title: onCallReceiverTitle,
|
||||
icon: onCallReceiverICon,
|
||||
};
|
||||
|
||||
export function getOnCallMetadata(
|
||||
export function useOnCallMetadata(
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
|
||||
receiver: GrafanaManagedReceiverConfig,
|
||||
hasAlertManagerConfigData = true
|
||||
): ReceiverPluginMetadata {
|
||||
const pluginName = getIsIrmPluginPresent() ? 'IRM' : 'OnCall';
|
||||
const { pluginId } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
return useMemo(
|
||||
() => getOnCallMetadata(onCallIntegrations, receiver, hasAlertManagerConfigData, pluginId),
|
||||
[onCallIntegrations, receiver, hasAlertManagerConfigData, pluginId]
|
||||
);
|
||||
}
|
||||
|
||||
export function getOnCallMetadata(
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
|
||||
receiver: GrafanaManagedReceiverConfig,
|
||||
hasAlertManagerConfigData = true,
|
||||
onCallPluginId?: SupportedPlugin
|
||||
): ReceiverPluginMetadata {
|
||||
const pluginId = onCallPluginId || SupportedPlugin.OnCall;
|
||||
const pluginName = pluginId === SupportedPlugin.Irm ? 'IRM' : 'OnCall';
|
||||
const onCallReceiverIcon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[pluginId];
|
||||
const onCallReceiverTitle = pluginId === SupportedPlugin.Irm ? 'Grafana IRM' : 'Grafana OnCall';
|
||||
|
||||
const onCallReceiverMeta: ReceiverPluginMetadata = {
|
||||
title: onCallReceiverTitle,
|
||||
icon: onCallReceiverIcon,
|
||||
};
|
||||
|
||||
if (!hasAlertManagerConfigData) {
|
||||
return onCallReceiverMeta;
|
||||
@@ -57,7 +74,7 @@ export function getOnCallMetadata(
|
||||
...onCallReceiverMeta,
|
||||
description: matchingOnCallIntegration?.display_name,
|
||||
externalUrl: matchingOnCallIntegration
|
||||
? createBridgeURL(getIrmIfPresentOrOnCallPluginId(), `/integrations/${matchingOnCallIntegration.value}`)
|
||||
? createBridgeURL(pluginId, `/integrations/${matchingOnCallIntegration.value}`)
|
||||
: undefined,
|
||||
warning: matchingOnCallIntegration ? undefined : `${pluginName} Integration no longer exists`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { pluginMeta } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { useIrmPlugin } from './usePluginBridge';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings');
|
||||
|
||||
const mockedGetPluginSettings = jest.mocked(getPluginSettings);
|
||||
|
||||
describe('useIrmPlugin', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when IRM plugin is installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve({ ...pluginMeta[SupportedPlugin.OnCall], enabled: false });
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return OnCall plugin ID when IRM plugin is not installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.OnCall]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.OnCall);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return Incident plugin ID when IRM plugin is not installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Incident) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Incident]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.Incident));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Incident);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return loading state while fetching plugins', () => {
|
||||
mockedGetPluginSettings.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve(pluginMeta[SupportedPlugin.Irm]), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.installed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return installed undefined when neither plugin is installed', async () => {
|
||||
mockedGetPluginSettings.mockRejectedValue(new Error('Plugin not found'));
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.OnCall);
|
||||
expect(result.current.installed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when both IRM and OnCall are installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.OnCall]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toEqual(pluginMeta[SupportedPlugin.Irm]);
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when both IRM and Incident are installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.Incident) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Incident]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.Incident));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toEqual(pluginMeta[SupportedPlugin.Irm]);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import { PluginMeta } from '@grafana/data';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { PluginID } from '../components/PluginBridge';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
interface PluginBridgeHookResponse {
|
||||
loading: boolean;
|
||||
installed?: boolean;
|
||||
@@ -14,17 +16,54 @@ interface PluginBridgeHookResponse {
|
||||
export function usePluginBridge(plugin: PluginID): PluginBridgeHookResponse {
|
||||
const { loading, error, value } = useAsync(() => getPluginSettings(plugin, { showErrorAlert: false }));
|
||||
|
||||
const installed = value && !error && !loading;
|
||||
const enabled = value?.enabled;
|
||||
const isLoading = loading && !value;
|
||||
|
||||
if (isLoading) {
|
||||
if (loading) {
|
||||
return { loading: true };
|
||||
}
|
||||
|
||||
if (!installed || !enabled) {
|
||||
return { loading: false, installed: false };
|
||||
if (error) {
|
||||
return { loading, error };
|
||||
}
|
||||
|
||||
return { loading, installed: true, settings: value };
|
||||
if (value) {
|
||||
return { loading, installed: value.enabled ?? false, settings: value };
|
||||
}
|
||||
|
||||
return { loading, installed: false };
|
||||
}
|
||||
|
||||
type FallbackPlugin = SupportedPlugin.OnCall | SupportedPlugin.Incident;
|
||||
type IrmWithFallback = SupportedPlugin.Irm | FallbackPlugin;
|
||||
|
||||
export interface PluginBridgeResult {
|
||||
pluginId: IrmWithFallback;
|
||||
loading: boolean;
|
||||
installed?: boolean;
|
||||
error?: Error;
|
||||
settings?: PluginMeta<{}>;
|
||||
}
|
||||
/**
|
||||
* Hook that checks for IRM plugin first, falls back to specified plugin.
|
||||
* IRM replaced both OnCall and Incident - this provides backward compatibility.
|
||||
*
|
||||
* @param fallback - The plugin to use if IRM is not installed (OnCall or Incident)
|
||||
* @returns Bridge result with the active plugin data
|
||||
*
|
||||
* @example
|
||||
* const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
*/
|
||||
export function useIrmPlugin(fallback: FallbackPlugin): PluginBridgeResult {
|
||||
const irmBridge = usePluginBridge(SupportedPlugin.Irm);
|
||||
const fallbackBridge = usePluginBridge(fallback);
|
||||
|
||||
const loading = irmBridge.loading || fallbackBridge.loading;
|
||||
const pluginId = irmBridge.installed ? SupportedPlugin.Irm : fallback;
|
||||
const activeBridge = irmBridge.installed ? irmBridge : fallbackBridge;
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
loading,
|
||||
installed: activeBridge.installed,
|
||||
error: activeBridge.error,
|
||||
settings: activeBridge.settings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import server from '@grafana/test-utils/server';
|
||||
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
|
||||
import {
|
||||
@@ -10,10 +9,7 @@ import {
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/alertmanagers';
|
||||
import { getFolderHandler } from 'app/features/alerting/unified/mocks/server/handlers/folders';
|
||||
import { listNamespacedTimeIntervalHandler } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
|
||||
import {
|
||||
getDisabledPluginHandler,
|
||||
getPluginMissingHandler,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/plugins';
|
||||
import { getDisabledPluginHandler } from 'app/features/alerting/unified/mocks/server/handlers/plugins';
|
||||
import {
|
||||
ALERTING_API_SERVER_BASE_URL,
|
||||
getK8sResponse,
|
||||
@@ -212,12 +208,6 @@ export function setGrafanaPromRules(groups: GrafanaPromRuleGroupDTO[]) {
|
||||
server.use(http.get(`/api/prometheus/grafana/api/v1/rules`, paginatedHandlerFor(groups)));
|
||||
}
|
||||
|
||||
/** Make a given plugin ID respond with a 404, as if it isn't installed at all */
|
||||
export const removePlugin = (pluginId: string) => {
|
||||
delete config.apps[pluginId];
|
||||
server.use(getPluginMissingHandler(pluginId));
|
||||
};
|
||||
|
||||
/** Make a plugin respond with `enabled: false`, as if its installed but disabled */
|
||||
export const disablePlugin = (pluginId: SupportedPlugin) => {
|
||||
clearPluginSettingsCache(pluginId);
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import {
|
||||
checkEvaluationIntervalGlobalLimit,
|
||||
getIrmIfPresentOrIncidentPluginId,
|
||||
getIrmIfPresentOrOnCallPluginId,
|
||||
getIsIrmPluginPresent,
|
||||
} from './config';
|
||||
import { checkEvaluationIntervalGlobalLimit } from './config';
|
||||
|
||||
describe('checkEvaluationIntervalGlobalLimit', () => {
|
||||
it('should NOT exceed limit if evaluate every is not valid duration', () => {
|
||||
@@ -59,48 +51,3 @@ describe('checkEvaluationIntervalGlobalLimit', () => {
|
||||
expect(exceedsLimit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIsIrmPluginPresent', () => {
|
||||
it('should return true when IRM plugin is present in config.apps', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIsIrmPluginPresent()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when IRM plugin is not present in config.apps', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIsIrmPluginPresent()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIrmIfPresentOrIncidentPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return Incident plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIrmIfPresentOrOnCallPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return OnCall plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
|
||||
|
||||
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||
@@ -28,15 +26,3 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
|
||||
|
||||
return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit };
|
||||
}
|
||||
|
||||
export function getIsIrmPluginPresent() {
|
||||
return SupportedPlugin.Irm in config.apps;
|
||||
}
|
||||
|
||||
export function getIrmIfPresentOrIncidentPluginId() {
|
||||
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.Incident;
|
||||
}
|
||||
|
||||
export function getIrmIfPresentOrOnCallPluginId() {
|
||||
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.OnCall;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrIncidentPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
import { useIrmPlugin } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
interface IncidentsPluginConfig {
|
||||
isInstalled: boolean;
|
||||
@@ -10,11 +10,13 @@ interface IncidentsPluginConfig {
|
||||
}
|
||||
|
||||
export function useGetIncidentPluginConfig(): IncidentsPluginConfig {
|
||||
const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(
|
||||
getIrmIfPresentOrIncidentPluginId()
|
||||
);
|
||||
const {
|
||||
pluginId,
|
||||
installed: incidentPluginInstalled,
|
||||
loading: loadingPluginSettings,
|
||||
} = useIrmPlugin(SupportedPlugin.Incident);
|
||||
const { data: incidentsConfig, isLoading: loadingPluginConfig } =
|
||||
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery();
|
||||
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery({ pluginId });
|
||||
|
||||
return {
|
||||
isInstalled: incidentPluginInstalled ?? false,
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import { onCallApi } from 'app/features/alerting/unified/api/onCallApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
import { useIrmPlugin } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
export function useGetOnCallIntegrations() {
|
||||
const { installed: onCallPluginInstalled } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
const { pluginId, installed: onCallPluginInstalled } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
|
||||
skip: !onCallPluginInstalled,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(
|
||||
{ pluginId },
|
||||
{
|
||||
skip: !onCallPluginInstalled,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
return onCallIntegrations ?? [];
|
||||
}
|
||||
|
||||
function useGetOnCallConfigurationChecks() {
|
||||
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(undefined, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
const { pluginId } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(
|
||||
{ pluginId },
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
||||
Reference in New Issue
Block a user