diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
index a8a5d70eeb6..8ffe1c4dd51 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
@@ -27,7 +27,7 @@ import {
} from '@grafana/ui';
import { alertSilencesApi, SilenceCreatedResponse } from 'app/features/alerting/unified/api/alertSilencesApi';
import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/constants';
-import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource';
+import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { MatcherOperator, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
import { SilenceFormFields } from '../../types/silence-form';
@@ -49,7 +49,7 @@ interface Props {
*
* Fetches silence details from API, based on `silenceId`
*/
-export const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => {
+const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => {
const {
data: silence,
isLoading: getSilenceIsLoading,
@@ -57,9 +57,12 @@ export const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Pro
} = alertSilencesApi.endpoints.getSilence.useQuery({
id: silenceId,
datasourceUid: getDatasourceAPIUid(alertManagerSourceName),
+ ruleMetadata: true,
+ accessControl: true,
});
const ruleUid = silence?.matchers?.find((m) => m.name === MATCHER_ALERT_RULE_UID)?.value;
+ const isGrafanaAlertManager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
const defaultValues = useMemo(() => {
if (!silence) {
@@ -80,6 +83,12 @@ export const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Pro
return
;
}
+ const canEditSilence = isGrafanaAlertManager ? silence?.accessControl?.write : true;
+
+ if (!canEditSilence) {
+ return
;
+ }
+
return (
);
@@ -258,6 +267,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
justifyContent: 'flex-start',
gap: theme.spacing(1),
maxWidth: theme.breakpoints.values.sm,
+ paddingTop: theme.spacing(2),
}),
});
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index bf03af22087..e37071e765c 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -1,14 +1,25 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
-import { dateMath, GrafanaTheme2 } from '@grafana/data';
-import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack, Alert, LoadingPlaceholder } from '@grafana/ui';
+import { GrafanaTheme2, dateMath } from '@grafana/data';
+import {
+ Alert,
+ CollapsableSection,
+ Divider,
+ Icon,
+ Link,
+ LinkButton,
+ LoadingPlaceholder,
+ Stack,
+ useStyles2,
+} from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
+import { Trans } from 'app/core/internationalization';
import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi';
import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
import { featureDiscoveryApi } from 'app/features/alerting/unified/api/featureDiscoveryApi';
-import { SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
-import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource';
+import { MATCHER_ALERT_RULE_UID, SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants';
+import { GRAFANA_RULES_SOURCE_NAME, getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource';
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
@@ -16,8 +27,6 @@ import { parseMatchers } from '../../utils/alertmanager';
import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc';
import { Authorize } from '../Authorize';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
-import { ActionButton } from '../rules/ActionButton';
-import { ActionIcon } from '../rules/ActionIcon';
import { Matchers } from './Matchers';
import { NoSilencesSplash } from './NoSilencesCTA';
@@ -49,7 +58,7 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
isLoading,
error,
} = alertSilencesApi.endpoints.getSilences.useQuery(
- { datasourceUid: getDatasourceAPIUid(alertManagerSourceName) },
+ { datasourceUid: getDatasourceAPIUid(alertManagerSourceName), ruleMetadata: true, accessControl: true },
API_QUERY_OPTIONS
);
@@ -102,8 +111,10 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
if (mimirLazyInitError) {
return (
- Create a new contact point to create a configuration using the default values or contact your administrator to
- set up the Alertmanager.
+
+ Create a new contact point to create a configuration using the default values or contact your administrator to
+ set up the Alertmanager.
+
);
}
@@ -125,7 +136,7 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
- Add Silence
+ Add Silence
@@ -138,7 +149,11 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
- Expired silences are automatically deleted after 5 days.
+
+
+ Expired silences are automatically deleted after 5 days.
+
+
}
+ renderExpandedContent={({ data }) => {
+ return (
+ <>
+
+
+ >
+ );
+ }}
/>
);
} else {
- return <>No matching silences found>;
+ return No matching silences found;;
}
}
@@ -238,6 +260,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
function useColumns(alertManagerSourceName: string) {
const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence);
const [expireSilence] = alertSilencesApi.endpoints.expireSilence.useMutation();
+ const isGrafanaFlavoredAlertmanager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (silenceId: string) => {
@@ -250,23 +273,40 @@ function useColumns(alertManagerSourceName: string) {
renderCell: function renderStateTag({ data: { status } }) {
return ;
},
- size: 4,
+ size: 3,
+ },
+ {
+ id: 'alert-rule',
+ label: 'Alert rule targeted',
+ renderCell: function renderAlertRuleLink({ data: { metadata } }) {
+ return metadata?.rule_title ? (
+
+ {metadata.rule_title}
+
+ ) : (
+ 'None'
+ );
+ },
+ size: 8,
},
{
id: 'matchers',
label: 'Matching labels',
renderCell: function renderMatchers({ data: { matchers } }) {
- return ;
+ const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || [];
+ return ;
},
- size: 10,
+ size: 7,
},
{
id: 'alerts',
- label: 'Alerts',
+ label: 'Alerts silenced',
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return {silencedAlerts.length};
},
- size: 4,
+ size: 2,
},
{
id: 'schedule',
@@ -275,39 +315,58 @@ function useColumns(alertManagerSourceName: string) {
const startsAtDate = dateMath.parse(startsAt);
const endsAtDate = dateMath.parse(endsAt);
const dateDisplayFormat = 'YYYY-MM-DD HH:mm';
- return (
- <>
- {' '}
- {startsAtDate?.format(dateDisplayFormat)} {'-'}
- {endsAtDate?.format(dateDisplayFormat)}
- >
- );
+ return `${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(dateDisplayFormat)}`;
},
size: 7,
},
];
- if (updateSupported && updateAllowed) {
+ if (updateSupported) {
columns.push({
id: 'actions',
label: 'Actions',
renderCell: function renderActions({ data: silence }) {
+ const isExpired = silence.status.state === SilenceState.Expired;
+
+ const canCreate = silence?.accessControl?.create;
+ const canWrite = silence?.accessControl?.write;
+
+ const canRecreate = isExpired && (isGrafanaFlavoredAlertmanager ? canCreate : updateAllowed);
+ const canEdit = !isExpired && (isGrafanaFlavoredAlertmanager ? canWrite : updateAllowed);
+
return (
-
- {silence.status.state === 'expired' ? (
-
- Recreate
-
- ) : (
- handleExpireSilenceClick(silence.id)}>
- Unsilence
-
+
+ {canRecreate && (
+
+ Recreate
+
)}
- {silence.status.state !== 'expired' && (
-
+ {canEdit && (
+ <>
+ handleExpireSilenceClick(silence.id)}
+ >
+ Unsilence
+
+
+ Edit
+
+ >
)}
);
@@ -316,6 +375,6 @@ function useColumns(alertManagerSourceName: string) {
});
}
return columns;
- }, [alertManagerSourceName, expireSilence, updateAllowed, updateSupported]);
+ }, [alertManagerSourceName, expireSilence, isGrafanaFlavoredAlertmanager, updateAllowed, updateSupported]);
}
export default SilencesTable;
diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx
index 6b4158a275b..b5c60260426 100644
--- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx
+++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx
@@ -3,22 +3,21 @@ import { createBrowserHistory } from 'history';
import React, { PropsWithChildren } from 'react';
import { Router } from 'react-router-dom';
import { TestProvider } from 'test/helpers/TestProvider';
+import { render, screen } from 'test/test-utils';
-import { mockFolderApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
-import {
- defaultGrafanaAlertingConfigurationStatusResponse,
- mockAlertmanagerChoiceResponse,
-} from 'app/features/alerting/unified/mocks/alertmanagerApi';
+import { setupMswServer } from 'app/features/alerting/unified/mockApi';
+import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
-import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+import { CombinedRule } from 'app/types/unified-alerting';
-import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource, mockFolder } from '../mocks';
+import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks';
import { AlertmanagerProvider } from '../state/AlertmanagerContext';
import { setupDataSources } from '../testSetup/datasources';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import {
+ AlertRuleAction,
AlertmanagerAction,
useAlertmanagerAbilities,
useAlertmanagerAbility,
@@ -142,20 +141,28 @@ describe('alertmanager abilities', () => {
});
});
+setupMswServer();
+
+/**
+ * Render the hook result in a component so we can more reliably check that the result has settled
+ * after API requests. Without this approach, the hook might return `[false, false]` whilst
+ * API requests are still loading
+ */
+const RenderActionPermissions = ({ rule, action }: { rule: CombinedRule; action: AlertRuleAction }) => {
+ const result = useAllAlertRuleAbilities(rule);
+ const [isSupported, isAllowed] = result[action];
+ return (
+ <>
+ {isSupported && 'supported'}
+ {isAllowed && 'allowed'}
+ >
+ );
+};
+
describe('AlertRule abilities', () => {
- const server = setupMswServer();
it('should report that all actions are supported for a Grafana Managed alert rule', async () => {
const rule = getGrafanaRule();
- // TODO: Remove server mocking within test once server is run before all tests
- mockFolderApi(server).folder(
- (rule.rulerRule as RulerGrafanaRuleDTO).grafana_alert.namespace_uid,
- mockFolder({
- accessControl: { [AccessControlAction.AlertingRuleUpdate]: false },
- })
- );
- mockAlertmanagerChoiceResponse(server, defaultGrafanaAlertingConfigurationStatusResponse);
-
const abilities = renderHook(() => useAllAlertRuleAbilities(rule), { wrapper: TestProvider });
await waitFor(() => {
@@ -169,6 +176,28 @@ describe('AlertRule abilities', () => {
expect(abilities.result.current).toMatchSnapshot();
});
+ it('grants correct silence permissions for folder with silence create permission', async () => {
+ setFolderAccessControl({ [AccessControlAction.AlertingSilenceCreate]: true });
+
+ const rule = getGrafanaRule();
+
+ render();
+
+ expect(await screen.findByText(/supported/)).toBeInTheDocument();
+ expect(await screen.findByText(/allowed/)).toBeInTheDocument();
+ });
+
+ it('does not grant silence permissions for folder without silence create permission', async () => {
+ setFolderAccessControl({ [AccessControlAction.AlertingSilenceCreate]: false });
+
+ const rule = getGrafanaRule();
+
+ render();
+
+ expect(await screen.findByText(/supported/)).toBeInTheDocument();
+ expect(screen.queryByText(/allowed/)).not.toBeInTheDocument();
+ });
+
it('should report no permissions while we are loading data for cloud rule', async () => {
const rule = getCloudRule();
diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts
index fa6a10da960..84f77d1d03f 100644
--- a/public/app/features/alerting/unified/hooks/useAbilities.ts
+++ b/public/app/features/alerting/unified/hooks/useAbilities.ts
@@ -1,9 +1,10 @@
import { useMemo } from 'react';
import { contextSrv as ctx } from 'app/core/services/context_srv';
+import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
-import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
+import { CombinedRule } from 'app/types/unified-alerting';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { useAlertmanager } from '../state/AlertmanagerContext';
@@ -170,7 +171,7 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities = {
[AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
@@ -272,7 +273,8 @@ export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability
* 1. the user has no permissions to create silences
* 2. the admin has configured to only send instances to external AMs
*/
-function useCanSilence(rulesSource: RulesSource): [boolean, boolean] {
+function useCanSilence(rule: CombinedRule): [boolean, boolean] {
+ const rulesSource = rule.namespace.rulesSource;
const isGrafanaManagedRule = rulesSource === GRAFANA_RULES_SOURCE_NAME;
const { currentData: amConfigStatus, isLoading } =
@@ -280,9 +282,12 @@ function useCanSilence(rulesSource: RulesSource): [boolean, boolean] {
skip: !isGrafanaManagedRule,
});
+ const folderUID = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.namespace_uid : undefined;
+ const { loading: folderIsLoading, folder } = useFolder(folderUID);
+
// we don't support silencing when the rule is not a Grafana managed rule
// we simply don't know what Alertmanager the ruler is sending alerts to
- if (!isGrafanaManagedRule || isLoading) {
+ if (!isGrafanaManagedRule || isLoading || folderIsLoading || !folder) {
return [false, false];
}
@@ -290,7 +295,16 @@ function useCanSilence(rulesSource: RulesSource): [boolean, boolean] {
const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All;
const silenceSupported = !interactsOnlyWithExternalAMs || interactsWithAll;
- return toAbility(silenceSupported, AccessControlAction.AlertingInstanceCreate);
+ const { accessControl = {} } = folder;
+
+ // User is permitted to silence if they either have the "global" permissions of "AlertingInstanceCreate",
+ // or the folder specific access control of "AlertingSilenceCreate"
+ const allowedToSilence = Boolean(
+ ctx.hasPermission(AccessControlAction.AlertingInstanceCreate) ||
+ accessControl[AccessControlAction.AlertingSilenceCreate]
+ );
+
+ return [silenceSupported, allowedToSilence];
}
// just a convenient function
diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts
index f32e2b5aaa5..bfd54535ded 100644
--- a/public/app/features/alerting/unified/mockApi.ts
+++ b/public/app/features/alerting/unified/mockApi.ts
@@ -10,6 +10,7 @@ import { DashboardDTO, FolderDTO, NotifierDTO, OrgUser } from 'app/types';
import {
PromBuildInfoResponse,
PromRulesResponse,
+ RulerGrafanaRuleDTO,
RulerRuleGroupDTO,
RulerRulesConfigDTO,
} from 'app/types/unified-alerting-dto';
@@ -283,6 +284,9 @@ export function mockAlertRuleApi(server: SetupServer) {
http.get(`/api/ruler/${dsName}/api/v1/rules/${namespace}/${group}`, () => HttpResponse.json(response))
);
},
+ getAlertRule: (uid: string, response: RulerGrafanaRuleDTO) => {
+ server.use(http.get(`/api/ruler/grafana/api/v1/rule/${uid}`, () => HttpResponse.json(response)));
+ },
};
}
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 30728ef3762..291de5444d4 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -20,6 +20,7 @@ import {
import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtime';
import { defaultDashboard } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
+import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
@@ -307,20 +308,46 @@ export const mockSilence = (partial: Partial = {}): Silence => {
status: {
state: SilenceState.Active,
},
+ accessControl: {
+ create: true,
+ read: true,
+ write: true,
+ },
...partial,
};
};
export const MOCK_SILENCE_ID_EXISTING = 'f209e273-0e4e-434f-9f66-e72f092025a2';
+export const MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID = '5f7d08cd-ac62-432e-8449-8c20c95c19b6';
+export const MOCK_SILENCE_ID_EXPIRED = '145884a8-ee20-4864-9f84-661305fb7d82';
+export const MOCK_SILENCE_ID_LACKING_PERMISSIONS = '31063317-f0d2-4d98-baf3-ec9febc1fa83';
export const mockSilences = [
- mockSilence({ id: MOCK_SILENCE_ID_EXISTING }),
+ mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }),
mockSilence({
id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7',
matchers: parseMatchers('foo!=bar'),
- comment: 'Catch all',
+ comment: 'Silence with negated matcher',
+ }),
+ mockSilence({
+ id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
+ matchers: parseMatchers(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`),
+ comment: 'Silence with alert rule UID matcher',
+ metadata: {
+ rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE,
+ },
+ }),
+ mockSilence({
+ id: MOCK_SILENCE_ID_LACKING_PERMISSIONS,
+ matchers: parseMatchers('something=else'),
+ comment: 'Silence without permissions to edit',
+ accessControl: {},
+ }),
+ mockSilence({
+ id: MOCK_SILENCE_ID_EXPIRED,
+ status: { state: SilenceState.Expired },
+ comment: 'Silence which is expired',
}),
- mockSilence({ id: '145884a8-ee20-4864-9f84-661305fb7d82', status: { state: SilenceState.Expired } }),
];
export const mockNotifiersState = (partial: Partial = {}): NotifiersState => {
diff --git a/public/app/features/alerting/unified/mocks/server/handlers/silences.ts b/public/app/features/alerting/unified/mocks/server/handlers/silences.ts
index 5e659aa15f7..432f1f9106e 100644
--- a/public/app/features/alerting/unified/mocks/server/handlers/silences.ts
+++ b/public/app/features/alerting/unified/mocks/server/handlers/silences.ts
@@ -4,22 +4,45 @@ import { mockSilences } from 'app/features/alerting/unified/mocks';
import { MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
const silencesListHandler = (silences = mockSilences) =>
- http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/silences', ({ params }) => {
+ http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/silences', ({ params, request }) => {
if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) {
return HttpResponse.json({ traceId: '' }, { status: 502 });
}
- return HttpResponse.json(silences);
+
+ // Server only responds with ACL/rule metadata if query param is sent
+ const accessControlQueryParam = new URL(request.url).searchParams.get('accesscontrol');
+ const ruleMetadataQueryParam = new URL(request.url).searchParams.get('ruleMetadata');
+
+ const mappedSilences = silences.map(({ accessControl, metadata, ...silence }) => {
+ return {
+ ...silence,
+ ...(accessControlQueryParam && { accessControl }),
+ ...(ruleMetadataQueryParam && { metadata }),
+ };
+ });
+
+ return HttpResponse.json(mappedSilences);
});
const silenceGetHandler = () =>
- http.get<{ uuid: string }>('/api/alertmanager/:datasourceUid/api/v2/silence/:uuid', ({ params }) => {
+ http.get<{ uuid: string }>('/api/alertmanager/:datasourceUid/api/v2/silence/:uuid', ({ params, request }) => {
const { uuid } = params;
const matchingMockSilence = mockSilences.find((silence) => silence.id === uuid);
- if (matchingMockSilence) {
- return HttpResponse.json(matchingMockSilence);
+ if (!matchingMockSilence) {
+ return HttpResponse.json({ message: 'silence not found' }, { status: 404 });
}
- return HttpResponse.json({ message: 'silence not found' }, { status: 404 });
+ // Server only responds with ACL/rule metadata if query param is sent
+ const accessControlQueryParam = new URL(request.url).searchParams.get('accesscontrol');
+ const ruleMetadataQueryParam = new URL(request.url).searchParams.get('ruleMetadata');
+
+ const { accessControl, metadata, ...silence } = matchingMockSilence;
+
+ return HttpResponse.json({
+ ...silence,
+ ...(accessControlQueryParam && { accessControl }),
+ ...(ruleMetadataQueryParam && { metadata }),
+ });
});
export const silenceCreateHandler = () =>
diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts
index 22f66078017..91ff5aa1fcc 100644
--- a/public/app/plugins/datasource/alertmanager/types.ts
+++ b/public/app/plugins/datasource/alertmanager/types.ts
@@ -1,5 +1,5 @@
//DOCS: https://prometheus.io/docs/alerting/latest/configuration/
-import { DataSourceJsonData } from '@grafana/data';
+import { DataSourceJsonData, WithAccessControlMetadata } from '@grafana/data';
export type AlertManagerCortexConfig = {
template_files: Record;
@@ -188,7 +188,7 @@ export enum MatcherOperator {
notRegex = '!~',
}
-export type Silence = {
+export interface Silence extends WithAccessControlMetadata {
id: string;
matchers?: Matcher[];
startsAt: string;
@@ -199,7 +199,12 @@ export type Silence = {
status: {
state: SilenceState;
};
-};
+ metadata?: {
+ rule_uid?: string;
+ rule_title?: string;
+ folder_uid?: string;
+ };
+}
export type SilenceCreatePayload = {
id?: string;
diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts
index 93e3a34e49f..c168d1c5272 100644
--- a/public/app/types/accessControl.ts
+++ b/public/app/types/accessControl.ts
@@ -101,6 +101,11 @@ export enum AccessControlAction {
AlertingInstanceUpdate = 'alert.instances:write',
AlertingInstanceRead = 'alert.instances:read',
+ // Alerting silences
+ AlertingSilenceCreate = 'alert.silences:create',
+ AlertingSilenceUpdate = 'alert.silences:write',
+ AlertingSilenceRead = 'alert.silences:read',
+
// Alerting Notification policies
AlertingNotificationsRead = 'alert.notifications:read',
AlertingNotificationsWrite = 'alert.notifications:write',
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index cff214e5231..6f0fec9ee34 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1669,6 +1669,21 @@
"empty-state": {
"button-title": "Create silence",
"title": "You haven't created any silences yet"
+ },
+ "table": {
+ "add-silence-button": "Add Silence",
+ "edit-button": "Edit",
+ "expired-silences": "Expired silences are automatically deleted after 5 days.",
+ "no-matching-silences": "No matching silences found;",
+ "noConfig": "Create a new contact point to create a configuration using the default values or contact your administrator to set up the Alertmanager.",
+ "recreate-button": "Recreate",
+ "unsilence-button": "Unsilence"
+ }
+ },
+ "silences-table": {
+ "header": {
+ "alert-name": "Alert name",
+ "state": "State"
}
},
"snapshot": {
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 8698cebe351..b796157a532 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1669,6 +1669,21 @@
"empty-state": {
"button-title": "Cřęäŧę şįľęʼnčę",
"title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny şįľęʼnčęş yęŧ"
+ },
+ "table": {
+ "add-silence-button": "Åđđ Ŝįľęʼnčę",
+ "edit-button": "Ēđįŧ",
+ "expired-silences": "Ēχpįřęđ şįľęʼnčęş äřę äūŧőmäŧįčäľľy đęľęŧęđ äƒŧęř 5 đäyş.",
+ "no-matching-silences": "Ńő mäŧčĥįʼnģ şįľęʼnčęş ƒőūʼnđ;",
+ "noConfig": "Cřęäŧę ä ʼnęŵ čőʼnŧäčŧ pőįʼnŧ ŧő čřęäŧę ä čőʼnƒįģūřäŧįőʼn ūşįʼnģ ŧĥę đęƒäūľŧ väľūęş őř čőʼnŧäčŧ yőūř äđmįʼnįşŧřäŧőř ŧő şęŧ ūp ŧĥę Åľęřŧmäʼnäģęř.",
+ "recreate-button": "Ŗęčřęäŧę",
+ "unsilence-button": "Ůʼnşįľęʼnčę"
+ }
+ },
+ "silences-table": {
+ "header": {
+ "alert-name": "Åľęřŧ ʼnämę",
+ "state": "Ŝŧäŧę"
}
},
"snapshot": {