mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 15:45:43 -06:00
Alerting: Add RBAC logic for silences creation (#87322)
* Remove role requirement for editing silence (instead handled by silence editor displaying error) * Send query params for metadata/access control silence logic * Add new access control types to enum * Add folder-specific checks for silences * Remove filtering in alert manager picker * fix flakey rule viewer test and update permissions helper * Use `returnTo` in rule viewer page * Fix incorrect display of duration * Clean up some mock server behaviour in rule details tests * Tweak styles for silences alerts table * Remove alertmanager picker from silences drawer * Add error if user cannot edit a silence * Show alert rule name in silences table and consume RBAC logic * Update mocks to include RBAC access control logic * Update silences tests * Update silences type to include access control info * Update comment for missing alertmanager * Update mock handlers and query param logic * Tweak query params again * Update access control mock * Revert AM picker fix as user may not have access to Grafana AM * Swap ternary order * Change text for no alert rule targeted * Don't show error alert from RTKQ query and remove alert instance preview in case of error * Add missing translations * Fix test adding missing mock for getting alert rule * Add missing translations in SilencesTable * Add translations autogenerated files * fix allowing edit silence in external alert manager --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com> Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
parent
20c90ff60d
commit
170d476bdc
@ -2463,13 +2463,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/silences/SilencedAlertsTable.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/silences/SilencedAlertsTableRow.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
@ -2490,14 +2485,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/silences/SilencesTable.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/alerting/unified/home/GettingStarted.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
|
@ -79,10 +79,6 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/alerting/silence/:id/edit',
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingInstanceUpdate,
|
||||
AccessControlAction.AlertingInstancesExternalWrite,
|
||||
]),
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||
),
|
||||
|
@ -1,47 +1,49 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor, userEvent, screen } from 'test/test-utils';
|
||||
import { render, screen, userEvent, waitFor, within } from 'test/test-utils';
|
||||
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { mockAlertRuleApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { waitForServerRequest } from 'app/features/alerting/unified/mocks/server/events';
|
||||
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
|
||||
import {
|
||||
MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER,
|
||||
MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER,
|
||||
MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/datasources';
|
||||
import { silenceCreateHandler } from 'app/features/alerting/unified/mocks/server/handlers/silences';
|
||||
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { MatcherOperator, SilenceState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import Silences from './Silences';
|
||||
import { grantUserPermissions, MOCK_SILENCE_ID_EXISTING, mockDataSource, MockDataSourceSrv } from './mocks';
|
||||
import { AlertmanagerProvider } from './state/AlertmanagerContext';
|
||||
import {
|
||||
MOCK_SILENCE_ID_EXISTING,
|
||||
MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
|
||||
MOCK_SILENCE_ID_LACKING_PERMISSIONS,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
mockSilences,
|
||||
} from './mocks';
|
||||
import { grafanaRulerRule } from './mocks/alertRuleApi';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
|
||||
jest.mock('app/core/services/context_srv');
|
||||
|
||||
const TEST_TIMEOUT = 60000;
|
||||
|
||||
const renderSilences = (location = '/alerting/silences/') => {
|
||||
locationService.push(location);
|
||||
return render(
|
||||
<AlertmanagerProvider accessType="instance">
|
||||
<Silences />
|
||||
</AlertmanagerProvider>,
|
||||
{
|
||||
historyOptions: {
|
||||
initialEntries: [location],
|
||||
},
|
||||
}
|
||||
);
|
||||
return render(<Silences />, {
|
||||
historyOptions: {
|
||||
initialEntries: [location],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const dataSources = {
|
||||
am: mockDataSource({
|
||||
name: 'Alertmanager',
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
[MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER]: mockDataSource({
|
||||
@ -61,6 +63,7 @@ const ui = {
|
||||
addSilenceButton: byRole('link', { name: /add silence/i }),
|
||||
queryBar: byPlaceholderText('Search'),
|
||||
existingSilenceNotFound: byRole('alert', { name: /existing silence .* not found/i }),
|
||||
noPermissionToEdit: byRole('alert', { name: /do not have permission/i }),
|
||||
editor: {
|
||||
timeRange: byTestId(selectors.components.TimePicker.openButton),
|
||||
durationField: byLabelText('Duration'),
|
||||
@ -74,6 +77,7 @@ const ui = {
|
||||
addMatcherButton: byRole('button', { name: 'Add matcher' }),
|
||||
submit: byText(/save silence/i),
|
||||
createdBy: byText(/created by \*/i),
|
||||
loadingIndicator: byTestId('Spinner'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -107,7 +111,7 @@ const addAdditionalMatcher = async () => {
|
||||
await user.click(ui.editor.addMatcherButton.get());
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
const server = setupMswServer();
|
||||
|
||||
beforeEach(() => {
|
||||
setupDataSources(dataSources.am, dataSources[MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER]);
|
||||
@ -117,10 +121,6 @@ describe('Silences', () => {
|
||||
beforeAll(resetMocks);
|
||||
afterEach(resetMocks);
|
||||
|
||||
beforeEach(() => {
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
});
|
||||
|
||||
it(
|
||||
'loads and shows silences',
|
||||
async () => {
|
||||
@ -133,10 +133,10 @@ describe('Silences', () => {
|
||||
expect(ui.expiredTable.get()).toBeInTheDocument();
|
||||
|
||||
const allSilences = ui.silenceRow.queryAll();
|
||||
expect(allSilences).toHaveLength(3);
|
||||
expect(allSilences[0]).toHaveTextContent('foo=bar');
|
||||
expect(allSilences[1]).toHaveTextContent('foo!=bar');
|
||||
expect(allSilences[2]).toHaveTextContent('foo=bar');
|
||||
expect(allSilences).toHaveLength(mockSilences.length);
|
||||
expect(within(allSilences[0]).getByLabelText('Tags')).toHaveTextContent('foo=bar');
|
||||
expect(within(allSilences[1]).getByLabelText('Tags')).toHaveTextContent('foo!=bar');
|
||||
expect(allSilences[2]).toHaveTextContent(MOCK_GRAFANA_ALERT_RULE_TITLE);
|
||||
|
||||
await user.click(ui.expiredCaret.get());
|
||||
|
||||
@ -144,7 +144,10 @@ describe('Silences', () => {
|
||||
expect(ui.expiredTable.query()).not.toBeInTheDocument();
|
||||
|
||||
const activeSilences = ui.silenceRow.queryAll();
|
||||
expect(activeSilences).toHaveLength(2);
|
||||
const expectedActiveSilences = mockSilences.filter(
|
||||
(silence) => silence.status.state !== SilenceState.Expired
|
||||
).length;
|
||||
expect(activeSilences).toHaveLength(expectedActiveSilences);
|
||||
expect(activeSilences[0]).toHaveTextContent('foo=bar');
|
||||
expect(activeSilences[1]).toHaveTextContent('foo!=bar');
|
||||
},
|
||||
@ -161,6 +164,7 @@ describe('Silences', () => {
|
||||
expect(notExpiredTable).toBeInTheDocument();
|
||||
|
||||
const silencedAlertRows = await ui.silencedAlertCell.findAll(notExpiredTable);
|
||||
|
||||
expect(silencedAlertRows[0]).toHaveTextContent('2');
|
||||
expect(silencedAlertRows[1]).toHaveTextContent('0');
|
||||
},
|
||||
@ -175,8 +179,8 @@ describe('Silences', () => {
|
||||
|
||||
const queryBar = await ui.queryBar.find();
|
||||
await user.type(queryBar, 'foo=bar');
|
||||
|
||||
await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(2));
|
||||
await screen.findByRole('button', { name: /clear filters/i });
|
||||
expect(ui.silenceRow.getAll()).toHaveLength(1);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
@ -211,6 +215,7 @@ describe('Silence create/edit', () => {
|
||||
afterEach(resetMocks);
|
||||
|
||||
beforeEach(() => {
|
||||
mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule);
|
||||
setUserLogged(true);
|
||||
});
|
||||
|
||||
@ -258,7 +263,7 @@ describe('Silence create/edit', () => {
|
||||
'creates a new silence',
|
||||
async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSilences(`${baseUrlPath}?alertmanager=Alertmanager`);
|
||||
renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
|
||||
expect(await ui.editor.durationField.find()).toBeInTheDocument();
|
||||
|
||||
const postRequest = waitForServerRequest(silenceCreateHandler());
|
||||
@ -314,6 +319,11 @@ describe('Silence create/edit', () => {
|
||||
expect(await ui.existingSilenceNotFound.find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an error when user cannot edit/recreate silence', async () => {
|
||||
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_LACKING_PERMISSIONS}/edit`);
|
||||
expect(await ui.noPermissionToEdit.find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('populates form with existing silence information', async () => {
|
||||
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING}/edit`);
|
||||
|
||||
@ -321,7 +331,13 @@ describe('Silence create/edit', () => {
|
||||
// existing fields have been filled out as well
|
||||
await waitFor(() => expect(ui.editor.matcherName.get()).toHaveValue('foo'));
|
||||
expect(ui.editor.matcherValue.get()).toHaveValue('bar');
|
||||
expect(ui.editor.comment.get()).toHaveValue('Silence noisy alerts');
|
||||
expect(ui.editor.comment.get()).toHaveValue('Happy path silence');
|
||||
});
|
||||
|
||||
it('populates form with existing silence information that has __alert_rule_uid__', async () => {
|
||||
mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule);
|
||||
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}/edit`);
|
||||
expect(await screen.findByLabelText(/alert rule/i)).toHaveValue(grafanaRulerRule.grafana_alert.title);
|
||||
});
|
||||
|
||||
it(
|
||||
@ -331,7 +347,7 @@ describe('Silence create/edit', () => {
|
||||
|
||||
const postRequest = waitForServerRequest(silenceCreateHandler());
|
||||
|
||||
renderSilences(`${baseUrlPath}?alertmanager=Alertmanager`);
|
||||
renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
|
||||
await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull());
|
||||
|
||||
await enterSilenceLabel(0, 'foo', MatcherOperator.equal, 'bar');
|
||||
@ -340,7 +356,7 @@ describe('Silence create/edit', () => {
|
||||
|
||||
expect(await ui.notExpiredTable.find()).toBeInTheDocument();
|
||||
|
||||
expect(locationService.getSearch().get('alertmanager')).toBe('Alertmanager');
|
||||
expect(locationService.getSearch().get('alertmanager')).toBe(GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
const createSilenceRequest = await postRequest;
|
||||
const requestBody = await createSilenceRequest.clone().json();
|
||||
|
@ -14,10 +14,17 @@ export const alertSilencesApi = alertingApi.injectEndpoints({
|
||||
Silence[],
|
||||
{
|
||||
datasourceUid: string;
|
||||
ruleMetadata?: boolean;
|
||||
accessControl?: boolean;
|
||||
}
|
||||
>({
|
||||
query: ({ datasourceUid }) => ({
|
||||
query: ({ datasourceUid, ruleMetadata, accessControl }) => ({
|
||||
url: `/api/alertmanager/${datasourceUid}/api/v2/silences`,
|
||||
params: {
|
||||
ruleMetadata,
|
||||
// query param is lowercased on backend for consistency with folder endpoint
|
||||
accesscontrol: accessControl,
|
||||
},
|
||||
}),
|
||||
providesTags: (result) =>
|
||||
result ? result.map(({ id }) => ({ type: 'AlertmanagerSilences', id })) : ['AlertmanagerSilences'],
|
||||
@ -28,11 +35,18 @@ export const alertSilencesApi = alertingApi.injectEndpoints({
|
||||
{
|
||||
datasourceUid: string;
|
||||
id: string;
|
||||
ruleMetadata?: boolean;
|
||||
accessControl?: boolean;
|
||||
}
|
||||
>({
|
||||
query: ({ datasourceUid, id }) => ({
|
||||
query: ({ datasourceUid, id, ruleMetadata, accessControl }) => ({
|
||||
url: `/api/alertmanager/${datasourceUid}/api/v2/silence/${id}`,
|
||||
showErrorAlert: false,
|
||||
params: {
|
||||
ruleMetadata,
|
||||
// query param is lowercased on backend for consistency with folder endpoint
|
||||
accesscontrol: accessControl,
|
||||
},
|
||||
}),
|
||||
providesTags: (result, error, { id }) =>
|
||||
result ? [{ type: 'AlertmanagerSilences', id }] : ['AlertmanagerSilences'],
|
||||
|
@ -4,13 +4,13 @@ import { dispatch } from 'app/store/store';
|
||||
import { ReceiversStateDTO } from 'app/types/alerting';
|
||||
|
||||
import {
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerAlert,
|
||||
AlertmanagerChoice,
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerGroup,
|
||||
GrafanaAlertingConfiguration,
|
||||
ExternalAlertmanagersConnectionStatus,
|
||||
ExternalAlertmanagersStatusResponse,
|
||||
GrafanaAlertingConfiguration,
|
||||
GrafanaManagedContactPoint,
|
||||
Matcher,
|
||||
MuteTimeInterval,
|
||||
@ -19,8 +19,8 @@ import { NotifierDTO } from '../../../../types';
|
||||
import { withPerformanceLogging } from '../Analytics';
|
||||
import { matcherToOperator } from '../utils/alertmanager';
|
||||
import {
|
||||
getDatasourceAPIUid,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
getDatasourceAPIUid,
|
||||
isVanillaPrometheusAlertManagerDataSource,
|
||||
} from '../utils/datasource';
|
||||
import { retryWhile, wrapWithQuotes } from '../utils/misc';
|
||||
@ -52,9 +52,9 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getAlertmanagerAlerts: build.query<
|
||||
AlertmanagerAlert[],
|
||||
{ amSourceName: string; filter?: AlertmanagerAlertsFilter }
|
||||
{ amSourceName: string; filter?: AlertmanagerAlertsFilter; showErrorAlert?: boolean }
|
||||
>({
|
||||
query: ({ amSourceName, filter }) => {
|
||||
query: ({ amSourceName, filter, showErrorAlert = true }) => {
|
||||
// TODO Add support for active, silenced, inhibited, unprocessed filters
|
||||
const filterMatchers = filter?.matchers
|
||||
?.filter((matcher) => matcher.name && matcher.value)
|
||||
@ -77,6 +77,7 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
return {
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(amSourceName)}/api/v2/alerts`,
|
||||
params,
|
||||
showErrorAlert,
|
||||
};
|
||||
},
|
||||
providesTags: ['AlertmanagerAlerts'],
|
||||
|
@ -9,43 +9,32 @@ import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/data
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* If true, only show alertmanagers that are receiving alerts from Grafana
|
||||
*/
|
||||
showOnlyReceivingGrafanaAlerts?: boolean;
|
||||
}
|
||||
|
||||
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
|
||||
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37);
|
||||
}
|
||||
|
||||
export const AlertManagerPicker = ({ disabled = false, showOnlyReceivingGrafanaAlerts }: Props) => {
|
||||
export const AlertManagerPicker = ({ disabled = false }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { selectedAlertmanager, availableAlertManagers, setSelectedAlertmanager } = useAlertmanager();
|
||||
|
||||
const options: Array<SelectableValue<string>> = useMemo(() => {
|
||||
return availableAlertManagers
|
||||
.filter(({ name, handleGrafanaManagedAlerts }) => {
|
||||
const isReceivingGrafanaAlerts = name === GRAFANA_RULES_SOURCE_NAME || handleGrafanaManagedAlerts;
|
||||
return showOnlyReceivingGrafanaAlerts ? isReceivingGrafanaAlerts : true;
|
||||
})
|
||||
.map((ds) => ({
|
||||
label: getAlertManagerLabel(ds),
|
||||
value: ds.name,
|
||||
imgUrl: ds.imgUrl,
|
||||
meta: ds.meta,
|
||||
}));
|
||||
}, [availableAlertManagers, showOnlyReceivingGrafanaAlerts]);
|
||||
return availableAlertManagers.map((ds) => ({
|
||||
label: getAlertManagerLabel(ds),
|
||||
value: ds.name,
|
||||
imgUrl: ds.imgUrl,
|
||||
meta: ds.meta,
|
||||
}));
|
||||
}, [availableAlertManagers]);
|
||||
|
||||
const isDisabled = disabled || options.length === 1;
|
||||
const label = isDisabled ? 'Alertmanager' : 'Choose Alertmanager';
|
||||
|
||||
return (
|
||||
<InlineField
|
||||
className={styles.field}
|
||||
label={disabled ? 'Alertmanager' : 'Choose Alertmanager'}
|
||||
disabled={disabled || options.length === 1}
|
||||
data-testid="alertmanager-picker"
|
||||
>
|
||||
<InlineField className={styles.field} label={label} disabled={isDisabled} data-testid="alertmanager-picker">
|
||||
<Select
|
||||
aria-label={disabled ? 'Alertmanager' : 'Choose Alertmanager'}
|
||||
aria-label={label}
|
||||
width={29}
|
||||
className="ds-picker select-container"
|
||||
backspaceRemovesValue={false}
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
import { grafanaRulerRule } from '../../mocks/alertRuleApi';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { DataSourceType } from '../../utils/datasource';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
|
||||
import { AlertRuleProvider } from './RuleContext';
|
||||
@ -71,8 +71,25 @@ setPluginExtensionsHook(() => ({
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Grants" permissions via contextSrv mock, and additionally sets folder access control
|
||||
* API response to match
|
||||
*/
|
||||
const grantPermissionsHelper = (permissions: AccessControlAction[]) => {
|
||||
const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {});
|
||||
grantUserPermissions(permissions);
|
||||
setFolderAccessControl(permissionsHash);
|
||||
};
|
||||
|
||||
const openSilenceDrawer = async () => {
|
||||
const user = userEvent.setup();
|
||||
await user.click(ELEMENTS.actions.more.button.get());
|
||||
await user.click(ELEMENTS.actions.more.actions.silence.get());
|
||||
await screen.findByText(/Configure silences/i);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([
|
||||
grantPermissionsHelper([
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
@ -109,23 +126,28 @@ describe('RuleViewer', () => {
|
||||
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([
|
||||
grantPermissionsHelper([
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleDelete,
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
AccessControlAction.AlertingInstancesExternalWrite,
|
||||
]);
|
||||
setBackendSrv(backendSrv);
|
||||
|
||||
setFolderAccessControl({
|
||||
[AccessControlAction.AlertingRuleCreate]: true,
|
||||
[AccessControlAction.AlertingRuleRead]: true,
|
||||
[AccessControlAction.AlertingRuleUpdate]: true,
|
||||
[AccessControlAction.AlertingRuleDelete]: true,
|
||||
[AccessControlAction.AlertingInstanceCreate]: true,
|
||||
});
|
||||
const dataSources = {
|
||||
am: mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
jsonData: {
|
||||
handleGrafanaManagedAlerts: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
setupDataSources(dataSources.am);
|
||||
});
|
||||
|
||||
it('should render a Grafana managed alert rule', async () => {
|
||||
@ -164,30 +186,9 @@ describe('RuleViewer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('renders silencing form correctly and shows alert rule name', async () => {
|
||||
const dataSources = {
|
||||
grafana: mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
type: DataSourceType.Alertmanager,
|
||||
jsonData: {
|
||||
handleGrafanaManagedAlerts: true,
|
||||
},
|
||||
}),
|
||||
am: mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
jsonData: {
|
||||
handleGrafanaManagedAlerts: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
setupDataSources(dataSources.grafana, dataSources.am);
|
||||
|
||||
it('renders silencing form correctly and shows alert rule name', async () => {
|
||||
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(ELEMENTS.actions.more.button.get());
|
||||
await user.click(ELEMENTS.actions.more.actions.silence.get());
|
||||
await openSilenceDrawer();
|
||||
|
||||
const silenceDrawer = await screen.findByRole('dialog', { name: 'Drawer title Silence alert rule' });
|
||||
expect(await within(silenceDrawer).findByLabelText(/^alert rule/i)).toHaveValue(
|
||||
|
@ -225,11 +225,13 @@ interface TitleProps {
|
||||
|
||||
export const Title = ({ name, paused = false, state, health, ruleType, ruleOrigin }: TitleProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [queryParams] = useQueryParams();
|
||||
const isRecordingRule = ruleType === PromRuleType.Recording;
|
||||
const returnTo = queryParams.returnTo ? String(queryParams.returnTo) : '/alerting/list';
|
||||
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
<LinkButton variant="secondary" icon="angle-left" href="/alerting/list" />
|
||||
<LinkButton variant="secondary" icon="angle-left" href={returnTo} />
|
||||
{ruleOrigin && <PluginOriginBadge pluginId={ruleOrigin.pluginId} />}
|
||||
<Text variant="h1" truncate>
|
||||
{name}
|
||||
|
@ -1,23 +1,14 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from 'test/test-utils';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import { usePluginLinkExtensions, setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
|
||||
import { GrafanaAlertingConfigurationStatusResponse } from '../../api/alertmanagerApi';
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
import { getCloudRule, getGrafanaRule } from '../../mocks';
|
||||
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
|
||||
@ -38,33 +29,16 @@ const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: /edit/i }),
|
||||
delete: byRole('button', { name: /delete/i }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
};
|
||||
|
||||
const server = setupServer(
|
||||
http.get(`/api/plugins/${SupportedPlugin.Incident}/settings`, async () => {
|
||||
return HttpResponse.json({
|
||||
enabled: false,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const alertmanagerChoiceMockedResponse: GrafanaAlertingConfigurationStatusResponse = {
|
||||
alertmanagersChoice: AlertmanagerChoice.Internal,
|
||||
numExternalAlertmanagers: 0,
|
||||
};
|
||||
setupMswServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.usePluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
@ -80,8 +54,6 @@ beforeEach(() => {
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
server.resetHandlers();
|
||||
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
|
||||
});
|
||||
|
||||
describe('RuleDetails RBAC', () => {
|
||||
@ -93,11 +65,10 @@ describe('RuleDetails RBAC', () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
render(<RuleDetails rule={grafanaRule} />);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users with the delete permission', async () => {
|
||||
@ -105,11 +76,10 @@ describe('RuleDetails RBAC', () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
render(<RuleDetails rule={grafanaRule} />);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||
});
|
||||
});
|
||||
|
||||
@ -121,11 +91,10 @@ describe('RuleDetails RBAC', () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
render(<RuleDetails rule={cloudRule} />);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users with the delete permission', async () => {
|
||||
@ -133,23 +102,10 @@ describe('RuleDetails RBAC', () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
render(<RuleDetails rule={cloudRule} />);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.queryByRole('button', { name: 'Declare incident' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderRuleDetails(rule: CombinedRule) {
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<RuleDetails rule={rule} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
@ -26,11 +26,15 @@ export const SilenceDetails = ({ silence }: Props) => {
|
||||
<div className={styles.title}>Schedule</div>
|
||||
<div>{`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(dateDisplayFormat)}`}</div>
|
||||
<div className={styles.title}>Duration</div>
|
||||
<div> {duration}</div>
|
||||
<div>{duration}</div>
|
||||
<div className={styles.title}>Created by</div>
|
||||
<div> {createdBy}</div>
|
||||
<div className={styles.title}>Affected alerts</div>
|
||||
<SilencedAlertsTable silencedAlerts={silencedAlerts} />
|
||||
<div>{createdBy}</div>
|
||||
{silencedAlerts.length > 0 && (
|
||||
<>
|
||||
<div className={styles.title}>Affected alerts</div>
|
||||
<SilencedAlertsTable silencedAlerts={silencedAlerts} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -40,6 +44,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 9fr',
|
||||
gridRowGap: '1rem',
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
title: css({
|
||||
color: theme.colors.text.primary,
|
||||
|
@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Divider, Drawer, Stack } from '@grafana/ui';
|
||||
import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from 'app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning';
|
||||
import { Drawer, Stack } from '@grafana/ui';
|
||||
import { SilencesEditor } from 'app/features/alerting/unified/components/silences/SilencesEditor';
|
||||
import { getDefaultSilenceFormValues } from 'app/features/alerting/unified/components/silences/utils';
|
||||
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
type Props = {
|
||||
@ -20,7 +18,6 @@ const SilenceGrafanaRuleDrawer = ({ rulerRule, onClose }: Props) => {
|
||||
const { uid } = rulerRule.grafana_alert;
|
||||
|
||||
const formValues = getDefaultSilenceFormValues();
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@ -30,17 +27,10 @@ const SilenceGrafanaRuleDrawer = ({ rulerRule, onClose }: Props) => {
|
||||
size="md"
|
||||
>
|
||||
<Stack direction={'column'}>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
|
||||
<div>
|
||||
<AlertManagerPicker showOnlyReceivingGrafanaAlerts />
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<SilencesEditor
|
||||
ruleUid={uid}
|
||||
formValues={formValues}
|
||||
alertManagerSourceName={selectedAlertmanager!}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onSilenceCreated={onClose}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
@ -29,9 +30,13 @@ const SilencedAlertsTable = ({ silencedAlerts }: Props) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>State</th>
|
||||
<th>
|
||||
<Trans i18nKey="silences-table.header.state">State</Trans>
|
||||
</th>
|
||||
<th></th>
|
||||
<th>Alert name</th>
|
||||
<th>
|
||||
<Trans i18nKey="silences-table.header.alert-name">Alert name</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -35,7 +35,7 @@ export const SilencedAlertsTableRow = ({ alert, className }: Props) => {
|
||||
<td>
|
||||
<AmAlertStateTag state={alert.status.state} />
|
||||
</td>
|
||||
<td>for {duration} seconds</td>
|
||||
<td>for {duration}</td>
|
||||
<td>{alertName}</td>
|
||||
</tr>
|
||||
{!isCollapsed && (
|
||||
|
@ -63,6 +63,14 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
|
||||
[amSourceName, matchers]
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert title="Preview not available" severity="error">
|
||||
Error occured when generating preview of affected alerts. Are your matchers valid?
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const tableItemAlerts = alerts.map<DynamicTableItemProps<AlertmanagerAlert>>((alert) => ({
|
||||
id: alert.fingerprint,
|
||||
data: alert,
|
||||
@ -77,11 +85,7 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
|
||||
) : null}
|
||||
</h4>
|
||||
{!hasValidMatchers && <span>Add a valid matcher to see affected alerts</span>}
|
||||
{isError && (
|
||||
<Alert title="Preview not available" severity="error">
|
||||
Error occured when generating preview of affected alerts. Are your matchers valid?
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isFetching && <LoadingPlaceholder text="Loading affected alert rule instances..." />}
|
||||
{!isFetching && !isError && hasValidMatchers && (
|
||||
<div className={styles.table}>
|
||||
|
@ -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 <Alert title={`Existing silence "${silenceId}" not found`} severity="warning" />;
|
||||
}
|
||||
|
||||
const canEditSilence = isGrafanaAlertManager ? silence?.accessControl?.write : true;
|
||||
|
||||
if (!canEditSilence) {
|
||||
return <Alert title={`You do not have permission to edit/recreate this silence`} severity="error" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SilencesEditor ruleUid={ruleUid} formValues={defaultValues} alertManagerSourceName={alertManagerSourceName} />
|
||||
);
|
||||
@ -258,6 +267,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.spacing(1),
|
||||
maxWidth: theme.breakpoints.values.sm,
|
||||
paddingTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -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 (
|
||||
<Alert title="The selected Alertmanager has no configuration" severity="warning">
|
||||
Create a new contact point to create a configuration using the default values or contact your administrator to
|
||||
set up the Alertmanager.
|
||||
<Trans i18nKey="silences.table.noConfig">
|
||||
Create a new contact point to create a configuration using the default values or contact your administrator to
|
||||
set up the Alertmanager.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@ -125,7 +136,7 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
|
||||
<Authorize actions={[AlertmanagerAction.CreateSilence]}>
|
||||
<Stack justifyContent="end">
|
||||
<LinkButton href={makeAMLink('/alerting/silence/new', alertManagerSourceName)} icon="plus">
|
||||
Add Silence
|
||||
<Trans i18nKey="silences.table.add-silence-button">Add Silence</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Authorize>
|
||||
@ -138,7 +149,11 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
|
||||
<CollapsableSection label={`Expired silences (${itemsExpired.length})`} isOpen={showExpiredFromUrl}>
|
||||
<div className={styles.callout}>
|
||||
<Icon className={styles.calloutIcon} name="info-circle" />
|
||||
<span>Expired silences are automatically deleted after 5 days.</span>
|
||||
<span>
|
||||
<Trans i18nKey="silences.table.expired-silences">
|
||||
Expired silences are automatically deleted after 5 days.
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
<SilenceList
|
||||
items={itemsExpired}
|
||||
@ -172,11 +187,18 @@ function SilenceList({
|
||||
cols={columns}
|
||||
isExpandable
|
||||
dataTestId={dataTestId}
|
||||
renderExpandedContent={({ data }) => <SilenceDetails silence={data} />}
|
||||
renderExpandedContent={({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<SilenceDetails silence={data} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <>No matching silences found</>;
|
||||
return <Trans i18nKey="silences.table.no-matching-silences">No matching silences found;</Trans>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 <SilenceStateTag state={status.state} />;
|
||||
},
|
||||
size: 4,
|
||||
size: 3,
|
||||
},
|
||||
{
|
||||
id: 'alert-rule',
|
||||
label: 'Alert rule targeted',
|
||||
renderCell: function renderAlertRuleLink({ data: { metadata } }) {
|
||||
return metadata?.rule_title ? (
|
||||
<Link
|
||||
href={`/alerting/grafana/${metadata?.rule_uid}/view?returnTo=${encodeURIComponent('/alerting/silences')}`}
|
||||
>
|
||||
{metadata.rule_title}
|
||||
</Link>
|
||||
) : (
|
||||
'None'
|
||||
);
|
||||
},
|
||||
size: 8,
|
||||
},
|
||||
{
|
||||
id: 'matchers',
|
||||
label: 'Matching labels',
|
||||
renderCell: function renderMatchers({ data: { matchers } }) {
|
||||
return <Matchers matchers={matchers || []} />;
|
||||
const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || [];
|
||||
return <Matchers matchers={filteredMatchers} />;
|
||||
},
|
||||
size: 10,
|
||||
size: 7,
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
label: 'Alerts',
|
||||
label: 'Alerts silenced',
|
||||
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
|
||||
return <span data-testid="alerts">{silencedAlerts.length}</span>;
|
||||
},
|
||||
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 (
|
||||
<Stack gap={0.5}>
|
||||
{silence.status.state === 'expired' ? (
|
||||
<Link href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}>
|
||||
<ActionButton icon="sync">Recreate</ActionButton>
|
||||
</Link>
|
||||
) : (
|
||||
<ActionButton icon="bell" onClick={() => handleExpireSilenceClick(silence.id)}>
|
||||
Unsilence
|
||||
</ActionButton>
|
||||
<Stack gap={0.5} wrap="wrap">
|
||||
{canRecreate && (
|
||||
<LinkButton
|
||||
title="Recreate"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="sync"
|
||||
href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
|
||||
>
|
||||
<Trans i18nKey="silences.table.recreate-button">Recreate</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
{silence.status.state !== 'expired' && (
|
||||
<ActionIcon
|
||||
to={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
|
||||
icon="pen"
|
||||
tooltip="Edit"
|
||||
/>
|
||||
{canEdit && (
|
||||
<>
|
||||
<LinkButton
|
||||
title="Unsilence"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="bell"
|
||||
onClick={() => handleExpireSilenceClick(silence.id)}
|
||||
>
|
||||
<Trans i18nKey="silences.table.unsilence-button">Unsilence</Trans>
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
title="Edit"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="pen"
|
||||
href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
|
||||
>
|
||||
<Trans i18nKey="silences.table.edit-button">Edit</Trans>
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
@ -316,6 +375,6 @@ function useColumns(alertManagerSourceName: string) {
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}, [alertManagerSourceName, expireSilence, updateAllowed, updateSupported]);
|
||||
}, [alertManagerSourceName, expireSilence, isGrafanaFlavoredAlertmanager, updateAllowed, updateSupported]);
|
||||
}
|
||||
export default SilencesTable;
|
||||
|
@ -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(<RenderActionPermissions rule={rule} action={AlertRuleAction.Silence} />);
|
||||
|
||||
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(<RenderActionPermissions rule={rule} action={AlertRuleAction.Silence} />);
|
||||
|
||||
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();
|
||||
|
||||
|
@ -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<AlertRul
|
||||
const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;
|
||||
|
||||
const rulesPermissions = getRulesPermissions(rulesSourceName);
|
||||
const canSilence = useCanSilence(rulesSource);
|
||||
const canSilence = useCanSilence(rule);
|
||||
|
||||
const abilities: Abilities<AlertRuleAction> = {
|
||||
[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
|
||||
|
@ -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)));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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> = {}): 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> = {}): NotifiersState => {
|
||||
|
@ -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 = () =>
|
||||
|
@ -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<string, string>;
|
||||
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user