mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: useAbilities hook (#72626)
This commit is contained in:
parent
07d96eb458
commit
bd23d48660
@ -1704,6 +1704,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/Authorize.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/Expression.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
@ -1825,9 +1829,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/utils/datasource.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/utils/misc.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
@ -5,21 +5,21 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import AlertGroups from './AlertGroups';
|
||||
import { fetchAlertGroups } from './api/alertmanager';
|
||||
import { mockAlertGroup, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv } from './mocks';
|
||||
import {
|
||||
grantUserPermissions,
|
||||
mockAlertGroup,
|
||||
mockAlertmanagerAlert,
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
} from './mocks';
|
||||
import { AlertmanagerProvider } from './state/AlertmanagerContext';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
|
||||
jest.mock('./api/alertmanager');
|
||||
|
||||
jest.mock('app/core/services/context_srv', () => ({
|
||||
contextSrv: {
|
||||
isEditor: true,
|
||||
hasAccess: () => true,
|
||||
hasPermission: () => true,
|
||||
},
|
||||
}));
|
||||
const mocks = {
|
||||
api: {
|
||||
fetchAlertGroups: jest.mocked(fetchAlertGroups),
|
||||
@ -29,7 +29,9 @@ const mocks = {
|
||||
const renderAmNotifications = () => {
|
||||
return render(
|
||||
<TestProvider>
|
||||
<AlertGroups />
|
||||
<AlertmanagerProvider accessType={'instance'}>
|
||||
<AlertGroups />
|
||||
</AlertmanagerProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
@ -57,6 +59,13 @@ const ui = {
|
||||
|
||||
describe('AlertGroups', () => {
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
]);
|
||||
|
||||
mocks.api.fetchAlertGroups.mockImplementation(() => {
|
||||
return Promise.resolve([
|
||||
mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }),
|
||||
|
@ -31,7 +31,6 @@ import { updateAlertManagerConfigAction } from './state/actions';
|
||||
import { FormAmRoute } from './types/amroutes';
|
||||
import { useRouteGroupsMatcher } from './useRouteGroupsMatcher';
|
||||
import { addUniqueIdentifierToRoute } from './utils/amroutes';
|
||||
import { isVanillaPrometheusAlertManagerDataSource } from './utils/datasource';
|
||||
import { normalizeMatchers } from './utils/matchers';
|
||||
import { computeInheritedTree } from './utils/notification-policies';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
@ -57,7 +56,7 @@ const AmRoutes = () => {
|
||||
const [labelMatchersFilter, setLabelMatchersFilter] = useState<ObjectMatcher[]>([]);
|
||||
|
||||
const { getRouteGroupsMap } = useRouteGroupsMatcher();
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const { selectedAlertmanager, hasConfigurationAPI } = useAlertmanager();
|
||||
|
||||
const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');
|
||||
|
||||
@ -186,10 +185,6 @@ const AmRoutes = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const vanillaPrometheusAlertManager = isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager);
|
||||
const readOnlyPolicies = vanillaPrometheusAlertManager;
|
||||
const readOnlyMuteTimings = vanillaPrometheusAlertManager;
|
||||
|
||||
const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;
|
||||
const haveData = result && !resultError && !resultLoading;
|
||||
const isFetching = !result && resultLoading;
|
||||
@ -246,7 +241,7 @@ const AmRoutes = () => {
|
||||
currentRoute={rootRoute}
|
||||
alertGroups={alertGroups ?? []}
|
||||
contactPointsState={contactPointsState.receivers}
|
||||
readOnly={readOnlyPolicies}
|
||||
readOnly={!hasConfigurationAPI}
|
||||
provisioned={isProvisioned}
|
||||
alertManagerSourceName={selectedAlertmanager}
|
||||
onAddPolicy={openAddModal}
|
||||
@ -265,7 +260,7 @@ const AmRoutes = () => {
|
||||
</>
|
||||
)}
|
||||
{muteTimingsTabActive && (
|
||||
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={readOnlyMuteTimings} />
|
||||
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={!hasConfigurationAPI} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -7,7 +7,6 @@ import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { DataSourceSrv, locationService, logInfo, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||
import * as actions from 'app/features/alerting/unified/state/actions';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -19,7 +18,6 @@ import { discoverFeatures } from './api/buildInfo';
|
||||
import { fetchRules } from './api/prometheus';
|
||||
import { deleteNamespace, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from './api/ruler';
|
||||
import {
|
||||
disableRBAC,
|
||||
enableRBAC,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
@ -137,7 +135,12 @@ beforeAll(() => {
|
||||
|
||||
describe('RuleList', () => {
|
||||
beforeEach(() => {
|
||||
contextSrv.isEditor = true;
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleExternalRead,
|
||||
AccessControlAction.AlertingRuleExternalWrite,
|
||||
]);
|
||||
mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
@ -147,7 +150,6 @@ describe('RuleList', () => {
|
||||
});
|
||||
|
||||
it('load & show rule groups from multiple cloud data sources', async () => {
|
||||
disableRBAC();
|
||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
|
@ -14,7 +14,7 @@ import { SilenceState } from '../../../plugins/datasource/alertmanager/types';
|
||||
|
||||
import Silences from './Silences';
|
||||
import { createOrUpdateSilence, fetchAlerts, fetchSilences } from './api/alertmanager';
|
||||
import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks';
|
||||
import { grantUserPermissions, mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks';
|
||||
import { parseMatchers } from './utils/alertmanager';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
|
||||
@ -98,19 +98,13 @@ const resetMocks = () => {
|
||||
|
||||
mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence());
|
||||
|
||||
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
|
||||
mocks.contextSrv.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
AccessControlAction.AlertingInstanceUpdate,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
AccessControlAction.AlertingInstancesExternalWrite,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
|
||||
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
AccessControlAction.AlertingInstanceUpdate,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
AccessControlAction.AlertingInstancesExternalWrite,
|
||||
]);
|
||||
};
|
||||
|
||||
const setUserLogged = (isLogged: boolean) => {
|
||||
@ -207,10 +201,7 @@ describe('Silences', () => {
|
||||
});
|
||||
|
||||
it('hides actions for creating a silence for users without access', async () => {
|
||||
mocks.contextSrv.hasAccess.mockImplementation((action) => {
|
||||
const permissions = [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
grantUserPermissions([AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead]);
|
||||
|
||||
renderSilences();
|
||||
await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
|
||||
|
@ -1,17 +1,73 @@
|
||||
import React from 'react';
|
||||
import { chain, filter } from 'lodash';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import {
|
||||
Abilities,
|
||||
Action,
|
||||
AlertmanagerAction,
|
||||
AlertSourceAction,
|
||||
useAlertSourceAbilities,
|
||||
useAllAlertmanagerAbilities,
|
||||
} from '../hooks/useAbilities';
|
||||
|
||||
type Props = {
|
||||
actions: AccessControlAction[];
|
||||
fallback?: boolean;
|
||||
interface AuthorizeProps extends PropsWithChildren {
|
||||
actions: AlertmanagerAction[] | AlertSourceAction[];
|
||||
}
|
||||
|
||||
export const Authorize = ({ actions, children }: AuthorizeProps) => {
|
||||
const alertmanagerActions = filter(actions, isAlertmanagerAction) as AlertmanagerAction[];
|
||||
const alertSourceActions = filter(actions, isAlertSourceAction) as AlertSourceAction[];
|
||||
|
||||
if (alertmanagerActions.length) {
|
||||
return <AuthorizeAlertmanager actions={alertmanagerActions}>{children}</AuthorizeAlertmanager>;
|
||||
}
|
||||
|
||||
if (alertSourceActions.length) {
|
||||
return <AuthorizeAlertsource actions={alertSourceActions}>{children}</AuthorizeAlertsource>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Authorize = ({ actions, children, fallback = true }: React.PropsWithChildren<Props>) => {
|
||||
if (actions.some((action) => contextSrv.hasAccess(action, fallback))) {
|
||||
interface ActionsProps<T extends Action> extends PropsWithChildren {
|
||||
actions: T[];
|
||||
}
|
||||
|
||||
const AuthorizeAlertmanager = ({ actions, children }: ActionsProps<AlertmanagerAction>) => {
|
||||
const alertmanagerAbilties = useAllAlertmanagerAbilities();
|
||||
const allowed = actionsAllowed(alertmanagerAbilties, actions);
|
||||
|
||||
if (allowed) {
|
||||
return <>{children}</>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const AuthorizeAlertsource = ({ actions, children }: ActionsProps<AlertSourceAction>) => {
|
||||
const alertSourceAbilities = useAlertSourceAbilities();
|
||||
const allowed = actionsAllowed(alertSourceAbilities, actions);
|
||||
|
||||
if (allowed) {
|
||||
return <>{children}</>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// check if some action is allowed from the abilities
|
||||
function actionsAllowed<T extends Action>(abilities: Abilities<T>, actions: T[]) {
|
||||
return chain(abilities)
|
||||
.pick(actions)
|
||||
.values()
|
||||
.value()
|
||||
.some(([_supported, allowed]) => allowed === true);
|
||||
}
|
||||
|
||||
function isAlertmanagerAction(action: AlertmanagerAction) {
|
||||
return Object.values(AlertmanagerAction).includes(action);
|
||||
}
|
||||
|
||||
function isAlertSourceAction(action: AlertSourceAction) {
|
||||
return Object.values(AlertSourceAction).includes(action);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { getInstancesPermissions } from '../../utils/access-control';
|
||||
import { AlertmanagerAction } from '../../hooks/useAbilities';
|
||||
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
||||
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
||||
@ -20,7 +20,6 @@ interface AmNotificationsAlertDetailsProps {
|
||||
|
||||
export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsAlertDetailsProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const instancePermissions = getInstancesPermissions(alertManagerSourceName);
|
||||
|
||||
// For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required
|
||||
// For external alert manager the Generator URL redirects to an external service which we don't control
|
||||
@ -32,8 +31,8 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
|
||||
return (
|
||||
<>
|
||||
<div className={styles.actionsRow}>
|
||||
<Authorize actions={[instancePermissions.update, instancePermissions.create]} fallback={contextSrv.isEditor}>
|
||||
{alert.status.state === AlertState.Suppressed && (
|
||||
{alert.status.state === AlertState.Suppressed && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateSilence, AlertmanagerAction.UpdateSilence]}>
|
||||
<LinkButton
|
||||
href={`${makeAMLink(
|
||||
'/alerting/silences',
|
||||
@ -45,8 +44,10 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
|
||||
>
|
||||
Manage silences
|
||||
</LinkButton>
|
||||
)}
|
||||
{alert.status.state === AlertState.Active && (
|
||||
</Authorize>
|
||||
)}
|
||||
{alert.status.state === AlertState.Active && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateSilence]}>
|
||||
<LinkButton
|
||||
href={makeLabelBasedSilenceLink(alertManagerSourceName, alert.labels)}
|
||||
className={styles.button}
|
||||
@ -55,8 +56,8 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
|
||||
>
|
||||
Silence
|
||||
</LinkButton>
|
||||
)}
|
||||
</Authorize>
|
||||
</Authorize>
|
||||
)}
|
||||
{isSeeSourceButtonEnabled && alert.generatorURL && (
|
||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||
See source
|
||||
|
@ -27,6 +27,7 @@ import * as receiversApi from '../../api/receiversApi';
|
||||
import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp';
|
||||
import { mockApi, setupMswServer } from '../../mockApi';
|
||||
import {
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
onCallPluginMetaMock,
|
||||
@ -86,14 +87,11 @@ const dataSources = {
|
||||
};
|
||||
|
||||
const renderReceivers = (alertManagerSourceName?: string) => {
|
||||
locationService.push(
|
||||
'/alerting/notifications' +
|
||||
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
|
||||
);
|
||||
locationService.push('/alerting/notifications');
|
||||
|
||||
return render(
|
||||
<TestProvider>
|
||||
<AlertmanagerProvider accessType="notification">
|
||||
<AlertmanagerProvider accessType="notification" alertmanagerSourceName={alertManagerSourceName}>
|
||||
<Receivers />
|
||||
</AlertmanagerProvider>
|
||||
</TestProvider>
|
||||
@ -167,26 +165,15 @@ describe('Receivers', () => {
|
||||
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });
|
||||
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.contextSrv.isEditor = true;
|
||||
|
||||
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
||||
|
||||
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
|
||||
mocks.contextSrv.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
|
||||
// respond with "true" when asked if we are an administrator
|
||||
mocks.contextSrv.hasRole.mockImplementation((role: string) => {
|
||||
return role === 'Admin';
|
||||
});
|
||||
|
||||
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => {
|
||||
@ -340,11 +327,10 @@ describe('Receivers', () => {
|
||||
|
||||
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
|
||||
mocks.api.updateConfig.mockResolvedValue();
|
||||
mocks.contextSrv.hasPermission.mockImplementation((action) =>
|
||||
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some(
|
||||
(a) => a === action
|
||||
)
|
||||
);
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
]);
|
||||
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
|
||||
renderReceivers();
|
||||
await ui.receiversTable.find();
|
||||
@ -523,7 +509,6 @@ describe('Receivers', () => {
|
||||
|
||||
expect(templatesTable).toBeInTheDocument();
|
||||
expect(receiversTable).toBeInTheDocument();
|
||||
expect(ui.newContactPointButton.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Contact points health', () => {
|
||||
|
@ -4,14 +4,13 @@ import React, { useMemo, useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { IconButton, LinkButton, Link, useStyles2, ConfirmModal } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { deleteMuteTimingAction } from '../../state/actions';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
|
||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||
@ -29,7 +28,6 @@ interface Props {
|
||||
export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hideActions }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
|
||||
const { currentData } = useAlertmanagerConfig(alertManagerSourceName, {
|
||||
refetchOnFocus: true,
|
||||
@ -57,6 +55,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
}, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
|
||||
|
||||
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
|
||||
const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -67,7 +66,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
</span>
|
||||
<Spacer />
|
||||
{!hideActions && items.length > 0 && (
|
||||
<Authorize actions={[permissions.create]}>
|
||||
<Authorize actions={[AlertmanagerAction.CreateMuteTiming]}>
|
||||
<LinkButton
|
||||
className={styles.addMuteButton}
|
||||
icon="plus"
|
||||
@ -88,7 +87,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
buttonIcon="plus"
|
||||
buttonSize="lg"
|
||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||
showButton={contextSrv.hasPermission(permissions.create)}
|
||||
showButton={allowedToCreateMuteTiming}
|
||||
/>
|
||||
) : (
|
||||
<EmptyAreaWithCTA text="No mute timings configured" buttonLabel={''} showButton={false} />
|
||||
@ -111,11 +110,11 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
};
|
||||
|
||||
function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) {
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
|
||||
const userHasEditPermissions = contextSrv.hasPermission(permissions.update);
|
||||
const userHasDeletePermissions = contextSrv.hasPermission(permissions.delete);
|
||||
const showActions = !hideActions && (userHasEditPermissions || userHasDeletePermissions);
|
||||
const [[_editSupported, allowedToEdit], [_deleteSupported, allowedToDelete]] = useAlertmanagerAbilities([
|
||||
AlertmanagerAction.UpdateMuteTiming,
|
||||
AlertmanagerAction.DeleteMuteTiming,
|
||||
]);
|
||||
const showActions = !hideActions && (allowedToEdit || allowedToDelete);
|
||||
|
||||
return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
|
||||
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
||||
@ -159,7 +158,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateMuteTiming]}>
|
||||
<Link
|
||||
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
|
||||
muteName: data.name,
|
||||
@ -168,7 +167,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
<IconButton name="edit" tooltip="Edit mute timing" />
|
||||
</Link>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<Authorize actions={[AlertmanagerAction.DeleteMuteTiming]}>
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
tooltip="Delete mute timing"
|
||||
@ -182,7 +181,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}, [alertManagerSourceName, setMuteTimingName, showActions, permissions]);
|
||||
}, [alertManagerSourceName, setMuteTimingName, showActions]);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { ReceiversState } from 'app/types/alerting';
|
||||
|
||||
import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { Policy } from './Policy';
|
||||
@ -221,7 +222,11 @@ describe('Policy', () => {
|
||||
});
|
||||
|
||||
const renderPolicy = (element: JSX.Element) =>
|
||||
render(<Router history={locationService.getHistory()}>{element}</Router>);
|
||||
render(
|
||||
<Router history={locationService.getHistory()}>
|
||||
<AlertmanagerProvider accessType="notification">{element}</AlertmanagerProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
const eq = MatcherOperator.equal;
|
||||
|
||||
|
@ -7,18 +7,17 @@ import { Link } from 'react-router-dom';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2, Text } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||
import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversState } from 'app/types';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { HoverCard } from '../HoverCard';
|
||||
import { Label } from '../Label';
|
||||
import { MetaText } from '../MetaText';
|
||||
@ -70,12 +69,15 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDefaultPolicy = currentRoute === routeTree;
|
||||
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const canEditRoutes = contextSrv.hasPermission(permissions.update);
|
||||
const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
|
||||
const canReadProvisioning =
|
||||
contextSrv.hasPermission(permissions.provisioning.read) ||
|
||||
contextSrv.hasPermission(permissions.provisioning.readSecrets);
|
||||
const [
|
||||
[updatePoliciesSupported, updatePoliciesAllowed],
|
||||
[deletePolicySupported, deletePolicyAllowed],
|
||||
[exportPoliciesSupported, exportPoliciesAllowed],
|
||||
] = useAlertmanagerAbilities([
|
||||
AlertmanagerAction.UpdateNotificationPolicyTree,
|
||||
AlertmanagerAction.DeleteNotificationPolicy,
|
||||
AlertmanagerAction.ExportNotificationPolicies,
|
||||
]);
|
||||
|
||||
const contactPoint = currentRoute.receiver;
|
||||
const continueMatching = currentRoute.continue ?? false;
|
||||
@ -116,9 +118,6 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0;
|
||||
const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;
|
||||
|
||||
const isEditable = canEditRoutes;
|
||||
const isDeletable = canDeleteRoutes && !isDefaultPolicy;
|
||||
|
||||
const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id);
|
||||
|
||||
// sum all alert instances for all groups we're handling
|
||||
@ -126,8 +125,59 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
|
||||
: undefined;
|
||||
|
||||
const isGrafanaAM = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
|
||||
const showExport = isGrafanaAM && isDefaultPolicy && canReadProvisioning;
|
||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy;
|
||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy;
|
||||
|
||||
// build the menu actions for our policy
|
||||
const dropdownMenuActions: JSX.Element[] = [];
|
||||
|
||||
if (showEditAction) {
|
||||
dropdownMenuActions.push(
|
||||
<Fragment key="edit-policy">
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Menu.Item
|
||||
icon="edit"
|
||||
disabled={provisioned}
|
||||
label="Edit"
|
||||
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (showExportAction) {
|
||||
dropdownMenuActions.push(
|
||||
<Menu.Item
|
||||
key="export-policy"
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
url={createUrl('/api/v1/provisioning/policies/export', {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
})}
|
||||
target="_blank"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showDeleteAction) {
|
||||
dropdownMenuActions.push(
|
||||
<Fragment key="delete-policy">
|
||||
<Menu.Divider />
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Menu.Item
|
||||
destructive
|
||||
icon="trash-alt"
|
||||
disabled={provisioned}
|
||||
label="Delete"
|
||||
onClick={() => onDeletePolicy(currentRoute)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated
|
||||
return (
|
||||
@ -155,9 +205,9 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
{/* TODO maybe we should move errors to the gutter instead? */}
|
||||
{errors.length > 0 && <Errors errors={errors} />}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
{readOnly && !showExport ? null : (
|
||||
{!readOnly && (
|
||||
<Stack direction="row" gap={0.5}>
|
||||
{!readOnly && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@ -170,58 +220,20 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
New nested policy
|
||||
</Button>
|
||||
</ConditionalWrap>
|
||||
)}
|
||||
</Authorize>
|
||||
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{!readOnly && (
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Menu.Item
|
||||
icon="edit"
|
||||
disabled={!isEditable || provisioned}
|
||||
label="Edit"
|
||||
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
)}
|
||||
{showExport && (
|
||||
<Menu.Item
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
url={createUrl('/api/v1/provisioning/policies/export', {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
})}
|
||||
target="_blank"
|
||||
/>
|
||||
)}
|
||||
{!readOnly && isDeletable && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Menu.Item
|
||||
destructive
|
||||
icon="trash-alt"
|
||||
disabled={!isDeletable || provisioned}
|
||||
label="Delete"
|
||||
onClick={() => onDeletePolicy(currentRoute)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon="ellipsis-h"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
{dropdownMenuActions.length > 0 && (
|
||||
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
||||
<Button
|
||||
icon="ellipsis-h"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -3,8 +3,8 @@ import React from 'react';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Alert, LinkButton } from '@grafana/ui';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { AlertmanagerAction } from '../../hooks/useAbilities';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { Authorize } from '../Authorize';
|
||||
@ -26,7 +26,7 @@ export const ReceiversAndTemplatesView = ({ config, alertManagerName }: Props) =
|
||||
<ReceiversTable config={config} alertManagerName={alertManagerName} />
|
||||
{!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />}
|
||||
{isCloud && (
|
||||
<Authorize actions={[AccessControlAction.AlertingNotificationsExternalWrite]}>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateExternalConfiguration]}>
|
||||
<Alert severity="info" title="Global config for contact points">
|
||||
<p>
|
||||
For each external Alertmanager you can define global settings, like server addresses, usernames and
|
||||
|
@ -14,6 +14,7 @@ import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } fr
|
||||
import { backendSrv } from '../../../../../core/services/backend_srv';
|
||||
import * as receiversApi from '../../api/receiversApi';
|
||||
import { enableRBAC, grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { createUrl } from '../../utils/url';
|
||||
@ -39,7 +40,9 @@ const renderReceieversTable = async (
|
||||
|
||||
return render(
|
||||
<TestProvider store={store}>
|
||||
<ReceiversTable config={config} alertManagerName={alertmanagerName} />
|
||||
<AlertmanagerProvider accessType={'notification'}>
|
||||
<ReceiversTable config={config} alertManagerName={alertmanagerName} />
|
||||
</AlertmanagerProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
|
@ -4,14 +4,16 @@ import React, { useMemo, useState } from 'react';
|
||||
import { dateTime, dateTimeFormat } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction, ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
|
||||
import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
|
||||
|
||||
import { isOrgAdmin } from '../../../../plugins/admin/permissions';
|
||||
import { useGetContactPointsState } from '../../api/receiversApi';
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { deleteReceiverAction } from '../../state/actions';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
@ -34,10 +36,10 @@ interface UpdateActionProps extends ActionProps {
|
||||
onClickDeleteReceiver: (receiverName: string) => void;
|
||||
}
|
||||
|
||||
function UpdateActions({ permissions, alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) {
|
||||
function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) {
|
||||
return (
|
||||
<>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
|
||||
<ActionIcon
|
||||
aria-label="Edit"
|
||||
data-testid="edit"
|
||||
@ -49,7 +51,7 @@ function UpdateActions({ permissions, alertManagerName, receiverName, onClickDel
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<Authorize actions={[AlertmanagerAction.DeleteContactPoint]}>
|
||||
<ActionIcon
|
||||
onClick={() => onClickDeleteReceiver(receiverName)}
|
||||
tooltip="Delete contact point"
|
||||
@ -61,23 +63,13 @@ function UpdateActions({ permissions, alertManagerName, receiverName, onClickDel
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
permissions: {
|
||||
read: AccessControlAction;
|
||||
create: AccessControlAction;
|
||||
update: AccessControlAction;
|
||||
delete: AccessControlAction;
|
||||
provisioning: {
|
||||
read: AccessControlAction;
|
||||
readSecrets: AccessControlAction;
|
||||
};
|
||||
};
|
||||
alertManagerName: string;
|
||||
receiverName: string;
|
||||
}
|
||||
|
||||
function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps) {
|
||||
function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
||||
return (
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
|
||||
<ActionIcon
|
||||
data-testid="view"
|
||||
to={makeAMLink(`/alerting/notifications/receivers/${encodeURIComponent(receiverName)}/edit`, alertManagerName)}
|
||||
@ -88,10 +80,14 @@ function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps
|
||||
);
|
||||
}
|
||||
|
||||
function ExportAction({ permissions, receiverName }: ActionProps) {
|
||||
const canReadSecrets = contextSrv.hasPermission(permissions.provisioning.readSecrets);
|
||||
function ExportAction({ receiverName }: ActionProps) {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const canReadSecrets = contextSrv.hasPermission(
|
||||
getNotificationsPermissions(selectedAlertmanager ?? '').provisioning.readSecrets
|
||||
);
|
||||
|
||||
return (
|
||||
<Authorize actions={[permissions.provisioning.read, permissions.provisioning.readSecrets]}>
|
||||
<Authorize actions={[AlertmanagerAction.ExportContactPoint]}>
|
||||
<ActionIcon
|
||||
data-testid="export"
|
||||
to={createUrl(`/api/v1/provisioning/contact-points/export/`, {
|
||||
@ -294,7 +290,6 @@ interface Props {
|
||||
export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
|
||||
const permissions = getNotificationsPermissions(alertManagerName);
|
||||
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
|
||||
|
||||
const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config);
|
||||
@ -305,11 +300,8 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
const [receiverToDelete, setReceiverToDelete] = useState<string>();
|
||||
const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false);
|
||||
|
||||
const isGrafanaAM = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
|
||||
const showExport =
|
||||
isGrafanaAM &&
|
||||
(contextSrv.hasPermission(permissions.provisioning.read) ||
|
||||
contextSrv.hasPermission(permissions.provisioning.readSecrets));
|
||||
const [supportsExport, allowedToExport] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
||||
const showExport = supportsExport && allowedToExport;
|
||||
|
||||
const onClickDeleteReceiver = (receiverName: string): void => {
|
||||
if (isReceiverUsed(receiverName, config)) {
|
||||
@ -355,15 +347,16 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
contactPointsState,
|
||||
configHealth,
|
||||
onClickDeleteReceiver,
|
||||
permissions,
|
||||
isVanillaAM
|
||||
);
|
||||
|
||||
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
||||
|
||||
return (
|
||||
<ReceiversSection
|
||||
title="Contact points"
|
||||
description="Define where notifications are sent, for example, email or Slack."
|
||||
showButton={!isVanillaAM && contextSrv.hasPermission(permissions.create)}
|
||||
showButton={createSupported && createAllowed}
|
||||
addButtonLabel={'Add contact point'}
|
||||
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
||||
exportLink={
|
||||
@ -439,16 +432,6 @@ function useGetColumns(
|
||||
contactPointsState: ContactPointsState | undefined,
|
||||
configHealth: AlertmanagerConfigHealth,
|
||||
onClickDeleteReceiver: (receiverName: string) => void,
|
||||
permissions: {
|
||||
read: AccessControlAction;
|
||||
create: AccessControlAction;
|
||||
update: AccessControlAction;
|
||||
delete: AccessControlAction;
|
||||
provisioning: {
|
||||
read: AccessControlAction;
|
||||
readSecrets: AccessControlAction;
|
||||
};
|
||||
},
|
||||
isVanillaAM: boolean
|
||||
): RowTableColumnProps[] {
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
@ -510,28 +493,21 @@ function useGetColumns(
|
||||
renderCell: ({ data: { provisioned, name } }) => (
|
||||
<Authorize
|
||||
actions={[
|
||||
permissions.update,
|
||||
permissions.delete,
|
||||
permissions.provisioning.read,
|
||||
permissions.provisioning.readSecrets,
|
||||
AlertmanagerAction.UpdateContactPoint,
|
||||
AlertmanagerAction.DeleteContactPoint,
|
||||
AlertmanagerAction.ExportContactPoint,
|
||||
]}
|
||||
fallback={isOrgAdmin()}
|
||||
>
|
||||
<div className={tableStyles.actionsCell}>
|
||||
{!isVanillaAM && !provisioned && (
|
||||
<UpdateActions
|
||||
permissions={permissions}
|
||||
alertManagerName={alertManagerName}
|
||||
receiverName={name}
|
||||
onClickDeleteReceiver={onClickDeleteReceiver}
|
||||
/>
|
||||
)}
|
||||
{(isVanillaAM || provisioned) && (
|
||||
<ViewAction permissions={permissions} alertManagerName={alertManagerName} receiverName={name} />
|
||||
)}
|
||||
{isGrafanaAlertManager && (
|
||||
<ExportAction permissions={permissions} alertManagerName={alertManagerName} receiverName={name} />
|
||||
)}
|
||||
{(isVanillaAM || provisioned) && <ViewAction alertManagerName={alertManagerName} receiverName={name} />}
|
||||
{isGrafanaAlertManager && <ExportAction alertManagerName={alertManagerName} receiverName={name} />}
|
||||
</div>
|
||||
</Authorize>
|
||||
),
|
||||
|
@ -4,11 +4,13 @@ import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
|
||||
import { TemplatesTable } from './TemplatesTable';
|
||||
|
||||
const defaultConfig: AlertManagerCortexConfig = {
|
||||
@ -25,7 +27,6 @@ jest.mock('app/types', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('app/core/services/context_srv');
|
||||
const contextSrvMock = jest.mocked(contextSrv);
|
||||
|
||||
const renderWithProvider = () => {
|
||||
const store = configureStore();
|
||||
@ -33,7 +34,9 @@ const renderWithProvider = () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<TemplatesTable config={defaultConfig} alertManagerName={'potato'} />
|
||||
<AlertmanagerProvider accessType={'notification'}>
|
||||
<TemplatesTable config={defaultConfig} alertManagerName={'potato'} />
|
||||
</AlertmanagerProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
@ -42,16 +45,12 @@ const renderWithProvider = () => {
|
||||
describe('TemplatesTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
contextSrvMock.hasAccess.mockImplementation(() => true);
|
||||
contextSrvMock.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
]);
|
||||
});
|
||||
it('Should render templates table with the correct rows', () => {
|
||||
renderWithProvider();
|
||||
@ -64,13 +63,11 @@ describe('TemplatesTable', () => {
|
||||
expect(within(rows[0]).getByRole('cell', { name: /Copy/i })).toBeInTheDocument();
|
||||
});
|
||||
it('Should not render duplicate template button when not having write permissions', () => {
|
||||
contextSrvMock.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
]);
|
||||
|
||||
renderWithProvider();
|
||||
const rows = screen.getAllByRole('row', { name: /template1/i });
|
||||
expect(within(rows[0]).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, { Fragment, useMemo, useState } from 'react';
|
||||
|
||||
import { ConfirmModal, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { deleteTemplateAction } from '../../state/actions';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
@ -27,7 +26,9 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
const permissions = getNotificationsPermissions(alertManagerName);
|
||||
const [createNotificationTemplateSupported, createNotificationTemplateAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.CreateNotificationTemplate
|
||||
);
|
||||
|
||||
const templateRows = useMemo(() => {
|
||||
const templates = Object.entries(config.template_files);
|
||||
@ -53,7 +54,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
description="Create notification templates to customize your notifications."
|
||||
addButtonLabel="Add template"
|
||||
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
|
||||
showButton={contextSrv.hasPermission(permissions.create)}
|
||||
showButton={createNotificationTemplateSupported && createNotificationTemplateAllowed}
|
||||
>
|
||||
<table className={tableStyles.table} data-testid="templates-table">
|
||||
<colgroup>
|
||||
@ -65,7 +66,13 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Template</th>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<Authorize
|
||||
actions={[
|
||||
AlertmanagerAction.CreateNotificationTemplate,
|
||||
AlertmanagerAction.UpdateNotificationTemplate,
|
||||
AlertmanagerAction.DeleteNotificationTemplate,
|
||||
]}
|
||||
>
|
||||
<th>Actions</th>
|
||||
</Authorize>
|
||||
</tr>
|
||||
@ -102,7 +109,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
/>
|
||||
)}
|
||||
{!provenance && (
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateNotificationTemplate]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
@ -113,7 +120,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
/>
|
||||
</Authorize>
|
||||
)}
|
||||
{contextSrv.hasPermission(permissions.create) && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateContactPoint]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/duplicate`,
|
||||
@ -122,10 +129,9 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
|
||||
tooltip="Copy template"
|
||||
icon="copy"
|
||||
/>
|
||||
)}
|
||||
|
||||
</Authorize>
|
||||
{!provenance && (
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<Authorize actions={[AlertmanagerAction.DeleteNotificationTemplate]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
|
@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import 'core-js/stable/structured-clone';
|
||||
@ -11,7 +10,7 @@ import { TestProvider } from '../../../../../../../test/helpers/TestProvider';
|
||||
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { Labels } from '../../../../../../types/unified-alerting-dto';
|
||||
import { mockApi, setupMswServer } from '../../../mockApi';
|
||||
import { mockAlertQuery } from '../../../mocks';
|
||||
import { grantUserPermissions, mockAlertQuery } from '../../../mocks';
|
||||
import { mockPreviewApiResponse } from '../../../mocks/alertRuleApi';
|
||||
import * as dataSource from '../../../utils/datasource';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
@ -34,8 +33,6 @@ jest.spyOn(notificationPreview, 'useGetAlertManagersSourceNamesAndImage').mockRe
|
||||
]);
|
||||
|
||||
jest.spyOn(dataSource, 'getDatasourceAPIUid').mockImplementation((ds: string) => ds);
|
||||
jest.mock('app/core/services/context_srv');
|
||||
const contextSrvMock = jest.mocked(contextSrv);
|
||||
|
||||
const useGetAlertManagersSourceNamesAndImageMock = useGetAlertManagersSourceNamesAndImage as jest.MockedFunction<
|
||||
typeof useGetAlertManagersSourceNamesAndImage
|
||||
@ -113,20 +110,19 @@ function mockTwoAlertManagers() {
|
||||
}
|
||||
|
||||
function mockHasEditPermission(enabled: boolean) {
|
||||
contextSrvMock.accessControlEnabled.mockReturnValue(true);
|
||||
contextSrvMock.hasAccess.mockImplementation((action) => {
|
||||
const onlyReadPermissions: string[] = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
];
|
||||
const readAndWritePermissions: string[] = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
];
|
||||
return enabled ? readAndWritePermissions.includes(action) : onlyReadPermissions.includes(action);
|
||||
});
|
||||
const onlyReadPermissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
];
|
||||
|
||||
const readAndWritePermissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
];
|
||||
|
||||
return enabled ? grantUserPermissions(readAndWritePermissions) : grantUserPermissions(onlyReadPermissions);
|
||||
}
|
||||
|
||||
const folder: Folder = {
|
||||
|
@ -7,7 +7,9 @@ import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack';
|
||||
import { getNotificationsPermissions } from '../../../utils/access-control';
|
||||
import { AlertmanagerAction } from '../../../hooks/useAbilities';
|
||||
import { AlertmanagerProvider } from '../../../state/AlertmanagerContext';
|
||||
import { GRAFANA_DATASOURCE_NAME } from '../../../utils/datasource';
|
||||
import { makeAMLink } from '../../../utils/misc';
|
||||
import { Authorize } from '../../Authorize';
|
||||
import { Matchers } from '../../notification-policies/Matchers';
|
||||
@ -57,54 +59,57 @@ export function NotificationRouteDetailsModal({
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDefault = isDefaultPolicy(route);
|
||||
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
return (
|
||||
<Modal
|
||||
className={styles.detailsModal}
|
||||
isOpen={true}
|
||||
title="Routing details"
|
||||
onDismiss={onClose}
|
||||
onClickBackdrop={onClose}
|
||||
>
|
||||
<Stack gap={0} direction="column">
|
||||
<div className={cx(styles.textMuted, styles.marginBottom(2))}>Your alert instances are routed as follows.</div>
|
||||
<div>Notification policy path</div>
|
||||
{isDefault && <div className={styles.textMuted}>Default policy</div>}
|
||||
<div className={styles.separator(1)} />
|
||||
{!isDefault && (
|
||||
<>
|
||||
<PolicyPath route={route} routesByIdMap={routesByIdMap} />
|
||||
</>
|
||||
)}
|
||||
<div className={styles.separator(4)} />
|
||||
<div className={styles.contactPoint}>
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
Contact point:
|
||||
<span className={styles.textMuted}>{receiver.name}</span>
|
||||
</Stack>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<AlertmanagerProvider accessType="notification" alertmanagerSourceName={GRAFANA_DATASOURCE_NAME}>
|
||||
<Modal
|
||||
className={styles.detailsModal}
|
||||
isOpen={true}
|
||||
title="Routing details"
|
||||
onDismiss={onClose}
|
||||
onClickBackdrop={onClose}
|
||||
>
|
||||
<Stack gap={0} direction="column">
|
||||
<div className={cx(styles.textMuted, styles.marginBottom(2))}>
|
||||
Your alert instances are routed as follows.
|
||||
</div>
|
||||
<div>Notification policy path</div>
|
||||
{isDefault && <div className={styles.textMuted}>Default policy</div>}
|
||||
<div className={styles.separator(1)} />
|
||||
{!isDefault && (
|
||||
<>
|
||||
<PolicyPath route={route} routesByIdMap={routesByIdMap} />
|
||||
</>
|
||||
)}
|
||||
<div className={styles.separator(4)} />
|
||||
<div className={styles.contactPoint}>
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
<a
|
||||
href={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerSourceName
|
||||
)}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See details <Icon name="external-link-alt" />
|
||||
</a>
|
||||
Contact point:
|
||||
<span className={styles.textMuted}>{receiver.name}</span>
|
||||
</Stack>
|
||||
</Authorize>
|
||||
</div>
|
||||
<div className={styles.button}>
|
||||
<Button variant="primary" type="button" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
<a
|
||||
href={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerSourceName
|
||||
)}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See details <Icon name="external-link-alt" />
|
||||
</a>
|
||||
</Stack>
|
||||
</Authorize>
|
||||
</div>
|
||||
<div className={styles.button}>
|
||||
<Button variant="primary" type="button" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,10 @@ import {
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { DateTimePicker, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { Authorize } from '../Authorize';
|
||||
import { VizWrapper } from '../rule-editor/VizWrapper';
|
||||
import { ThresholdDefinition } from '../rule-editor/util';
|
||||
|
||||
@ -64,6 +63,8 @@ export function RuleViewerVisualization({
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowedToExploreDataSources = contextSrv.hasAccessToExplore();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={styles.header}>
|
||||
@ -71,19 +72,18 @@ export function RuleViewerVisualization({
|
||||
{!isExpression && relativeTimeRange ? (
|
||||
<DateTimePicker date={setDateTime(relativeTimeRange.to)} onChange={onTimeChange} maxDate={new Date()} />
|
||||
) : null}
|
||||
<Authorize actions={[AccessControlAction.DataSourcesExplore]}>
|
||||
{!isExpression && (
|
||||
<LinkButton
|
||||
size="md"
|
||||
variant="secondary"
|
||||
icon="compass"
|
||||
target="_blank"
|
||||
href={createExploreLink(dsSettings, model)}
|
||||
>
|
||||
View in Explore
|
||||
</LinkButton>
|
||||
)}
|
||||
</Authorize>
|
||||
|
||||
{allowedToExploreDataSources && !isExpression && (
|
||||
<LinkButton
|
||||
size="md"
|
||||
variant="secondary"
|
||||
icon="compass"
|
||||
target="_blank"
|
||||
href={createExploreLink(dsSettings, model)}
|
||||
>
|
||||
View in Explore
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<VizWrapper data={data} thresholds={thresholds?.config} thresholdsType={thresholds?.mode} />
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { logInfo } from '@grafana/runtime';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { LogMessages } from '../../Analytics';
|
||||
import { AlertSourceAction } from '../../hooks/useAbilities';
|
||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { Authorize } from '../Authorize';
|
||||
|
||||
@ -36,10 +36,10 @@ export const RuleListGroupView = ({ namespaces, expandAll }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Authorize actions={[AccessControlAction.AlertingRuleRead]}>
|
||||
<Authorize actions={[AlertSourceAction.ViewAlertRule]}>
|
||||
<GrafanaRules namespaces={grafanaNamespaces} expandAll={expandAll} />
|
||||
</Authorize>
|
||||
<Authorize actions={[AccessControlAction.AlertingRuleExternalRead]}>
|
||||
<Authorize actions={[AlertSourceAction.ViewExternalAlertRule]}>
|
||||
<CloudRules namespaces={cloudNamespaces} expandAll={expandAll} />
|
||||
</Authorize>
|
||||
</>
|
||||
|
@ -5,12 +5,11 @@ import { dateMath, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { CollapsableSection, Icon, Link, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { expireSilenceAction } from '../../state/actions';
|
||||
import { getInstancesPermissions } from '../../utils/access-control';
|
||||
import { parseMatchers } from '../../utils/alertmanager';
|
||||
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
|
||||
import { Authorize } from '../Authorize';
|
||||
@ -41,7 +40,6 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }:
|
||||
const [queryParams] = useQueryParams();
|
||||
const filteredSilencesNotExpired = useFilteredSilences(silences, false);
|
||||
const filteredSilencesExpired = useFilteredSilences(silences, true);
|
||||
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||
|
||||
const { silenceState: silenceStateInParams } = getSilenceFiltersFromUrlParams(queryParams);
|
||||
const showExpiredFromUrl = silenceStateInParams === SilenceState.Expired;
|
||||
@ -77,7 +75,7 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }:
|
||||
{!!silences.length && (
|
||||
<Stack direction="column">
|
||||
<SilencesFilter />
|
||||
<Authorize actions={[permissions.create]} fallback={contextSrv.isEditor}>
|
||||
<Authorize actions={[AlertmanagerAction.CreateSilence]}>
|
||||
<div className={styles.topButtonContainer}>
|
||||
<LinkButton href={makeAMLink('/alerting/silence/new', alertManagerSourceName)} icon="plus">
|
||||
Add Silence
|
||||
@ -205,12 +203,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
function useColumns(alertManagerSourceName: string) {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||
const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence);
|
||||
|
||||
return useMemo((): SilenceTableColumnProps[] => {
|
||||
const handleExpireSilenceClick = (id: string) => {
|
||||
dispatch(expireSilenceAction(alertManagerSourceName, id));
|
||||
};
|
||||
const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
|
||||
const columns: SilenceTableColumnProps[] = [
|
||||
{
|
||||
id: 'state',
|
||||
@ -254,7 +252,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
size: 7,
|
||||
},
|
||||
];
|
||||
if (showActions) {
|
||||
if (updateSupported && updateAllowed) {
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
@ -285,6 +283,6 @@ function useColumns(alertManagerSourceName: string) {
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}, [alertManagerSourceName, dispatch, styles, permissions]);
|
||||
}, [alertManagerSourceName, dispatch, styles.editButton, updateAllowed, updateSupported]);
|
||||
}
|
||||
export default SilencesTable;
|
||||
|
@ -0,0 +1,292 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`alertmanager abilities should report Create / Update / Delete actions aren't supported for external vanilla alertmanager 1`] = `
|
||||
{
|
||||
"create-contact-point": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"create-mute-timing": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"create-notification-policy": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"create-notification-template": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"create-silence": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-mute-timing": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-notification-policy": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-notification-template": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"edit-contact-points": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"edit-notification-template": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"export-contact-point": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"update-external-configuration": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"update-mute-timing": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"update-notification-policy-tree": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"update-silence": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-external-configuration": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-mute-timing": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-notification-policy-tree": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-notification-template": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-silence": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`alertmanager abilities should report everything except exporting for Mimir alertmanager 1`] = `
|
||||
{
|
||||
"create-contact-point": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"create-mute-timing": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"create-notification-policy": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"create-notification-template": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"create-silence": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"delete-mute-timing": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"delete-notification-policy": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"delete-notification-template": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"edit-contact-points": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"edit-notification-template": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"export-contact-point": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"update-external-configuration": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"update-mute-timing": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"update-notification-policy-tree": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"update-silence": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-external-configuration": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-mute-timing": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-notification-policy-tree": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-notification-template": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-silence": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`alertmanager abilities should report everything is supported for builtin alertmanager 1`] = `
|
||||
{
|
||||
"create-contact-point": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"create-mute-timing": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"create-notification-policy": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"create-notification-template": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"create-silence": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"delete-mute-timing": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"delete-notification-policy": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"delete-notification-template": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"edit-contact-points": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"edit-notification-template": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"export-contact-point": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"update-external-configuration": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"update-mute-timing": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"update-notification-policy-tree": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"update-silence": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-external-configuration": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-mute-timing": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-notification-policy-tree": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-notification-template": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-silence": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
}
|
||||
`;
|
145
public/app/features/alerting/unified/hooks/useAbilities.test.tsx
Normal file
145
public/app/features/alerting/unified/hooks/useAbilities.test.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { grantUserPermissions, mockDataSource } from '../mocks';
|
||||
import { AlertmanagerProvider } from '../state/AlertmanagerContext';
|
||||
import { setupDataSources } from '../testSetup/datasources';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
import {
|
||||
AlertmanagerAction,
|
||||
useAlertmanagerAbilities,
|
||||
useAlertmanagerAbility,
|
||||
useAllAlertmanagerAbilities,
|
||||
} from './useAbilities';
|
||||
|
||||
/**
|
||||
* This test will write snapshots with a record of the current permissions assigned to actions.
|
||||
* We encourage that every change to the snapshot is inspected _very_ thoroughly!
|
||||
*/
|
||||
describe('alertmanager abilities', () => {
|
||||
it("should report Create / Update / Delete actions aren't supported for external vanilla alertmanager", () => {
|
||||
setupDataSources(
|
||||
mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
type: DataSourceType.Alertmanager,
|
||||
jsonData: { implementation: AlertManagerImplementation.prometheus },
|
||||
})
|
||||
);
|
||||
|
||||
const abilities = renderHook(() => useAllAlertmanagerAbilities(), { wrapper: createWrapper('does-not-exist') });
|
||||
expect(abilities.result.current).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should report everything is supported for builtin alertmanager', () => {
|
||||
setupDataSources(
|
||||
mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
type: DataSourceType.Alertmanager,
|
||||
})
|
||||
);
|
||||
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingInstanceRead]);
|
||||
|
||||
const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
|
||||
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
|
||||
});
|
||||
|
||||
Object.values(abilities.result.current).forEach(([supported]) => {
|
||||
expect(supported).toBe(true);
|
||||
});
|
||||
|
||||
// since we only granted "read" permissions, only those should be allowed
|
||||
const viewAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
|
||||
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
|
||||
});
|
||||
|
||||
const [viewSupported, viewAllowed] = viewAbility.result.current;
|
||||
|
||||
expect(viewSupported).toBe(true);
|
||||
expect(viewAllowed).toBe(true);
|
||||
|
||||
// editing should not be allowed, but supported
|
||||
const editAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
|
||||
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
|
||||
});
|
||||
|
||||
const [editSupported, editAllowed] = editAbility.result.current;
|
||||
|
||||
expect(editSupported).toBe(true);
|
||||
expect(editAllowed).toBe(true);
|
||||
|
||||
// record the snapshot to prevent future regressions
|
||||
expect(abilities.result.current).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should report everything except exporting for Mimir alertmanager', () => {
|
||||
setupDataSources(
|
||||
mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: 'mimir',
|
||||
type: DataSourceType.Alertmanager,
|
||||
jsonData: {
|
||||
implementation: AlertManagerImplementation.mimir,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
AccessControlAction.AlertingInstancesExternalRead,
|
||||
AccessControlAction.AlertingInstancesExternalWrite,
|
||||
]);
|
||||
|
||||
const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
|
||||
wrapper: createWrapper('mimir'),
|
||||
});
|
||||
|
||||
expect(abilities.result.current).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be able to return multiple abilities', () => {
|
||||
setupDataSources(
|
||||
mockDataSource<AlertManagerDataSourceJsonData>({
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
type: DataSourceType.Alertmanager,
|
||||
})
|
||||
);
|
||||
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
|
||||
const abilities = renderHook(
|
||||
() =>
|
||||
useAlertmanagerAbilities([
|
||||
AlertmanagerAction.ViewContactPoint,
|
||||
AlertmanagerAction.CreateContactPoint,
|
||||
AlertmanagerAction.ExportContactPoint,
|
||||
]),
|
||||
{
|
||||
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
|
||||
}
|
||||
);
|
||||
|
||||
expect(abilities.result.current).toHaveLength(3);
|
||||
expect(abilities.result.current[0]).toStrictEqual([true, true]);
|
||||
expect(abilities.result.current[1]).toStrictEqual([true, false]);
|
||||
expect(abilities.result.current[2]).toStrictEqual([true, false]);
|
||||
});
|
||||
});
|
||||
|
||||
function createWrapper(alertmanagerSourceName: string) {
|
||||
const wrapper = (props: PropsWithChildren) => (
|
||||
<Router history={createBrowserHistory()}>
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertmanagerSourceName}>
|
||||
{props.children}
|
||||
</AlertmanagerProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
}
|
205
public/app/features/alerting/unified/hooks/useAbilities.ts
Normal file
205
public/app/features/alerting/unified/hooks/useAbilities.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { contextSrv as ctx } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { getInstancesPermissions, getNotificationsPermissions } from '../utils/access-control';
|
||||
|
||||
/**
|
||||
* These hooks will determine if
|
||||
* 1. the action is supported in the current alertmanager or data source context
|
||||
* 2. user is allowed to perform actions based on their set of permissions / assigned role
|
||||
*/
|
||||
export enum AlertmanagerAction {
|
||||
// configuration
|
||||
ViewExternalConfiguration = 'view-external-configuration',
|
||||
UpdateExternalConfiguration = 'update-external-configuration',
|
||||
|
||||
// contact points
|
||||
CreateContactPoint = 'create-contact-point',
|
||||
ViewContactPoint = 'view-contact-point',
|
||||
UpdateContactPoint = 'edit-contact-points',
|
||||
DeleteContactPoint = 'delete-contact-point',
|
||||
ExportContactPoint = 'export-contact-point',
|
||||
|
||||
// notification templates
|
||||
CreateNotificationTemplate = 'create-notification-template',
|
||||
ViewNotificationTemplate = 'view-notification-template',
|
||||
UpdateNotificationTemplate = 'edit-notification-template',
|
||||
DeleteNotificationTemplate = 'delete-notification-template',
|
||||
|
||||
// notification policies
|
||||
CreateNotificationPolicy = 'create-notification-policy',
|
||||
ViewNotificationPolicyTree = 'view-notification-policy-tree',
|
||||
UpdateNotificationPolicyTree = 'update-notification-policy-tree',
|
||||
DeleteNotificationPolicy = 'delete-notification-policy',
|
||||
ExportNotificationPolicies = 'export-notification-policies',
|
||||
|
||||
// silences – these cannot be deleted only "expired" (updated)
|
||||
CreateSilence = 'create-silence',
|
||||
ViewSilence = 'view-silence',
|
||||
UpdateSilence = 'update-silence',
|
||||
|
||||
// mute timings
|
||||
ViewMuteTiming = 'view-mute-timing',
|
||||
CreateMuteTiming = 'create-mute-timing',
|
||||
UpdateMuteTiming = 'update-mute-timing',
|
||||
DeleteMuteTiming = 'delete-mute-timing',
|
||||
}
|
||||
|
||||
export enum AlertSourceAction {
|
||||
// internal (Grafana managed)
|
||||
CreateAlertRule = 'create-alert-rule',
|
||||
ViewAlertRule = 'view-alert-rule',
|
||||
UpdateAlertRule = 'update-alert-rule',
|
||||
DeleteAlertRule = 'delete-alert-rule',
|
||||
// external (any compatible alerting data source)
|
||||
CreateExternalAlertRule = 'create-external-alert-rule',
|
||||
ViewExternalAlertRule = 'view-external-alert-rule',
|
||||
UpdateExternalAlertRule = 'update-external-alert-rule',
|
||||
DeleteExternalAlertRule = 'delete-external-alert-rule',
|
||||
}
|
||||
|
||||
const AlwaysSupported = true; // this just makes it easier to understand the code
|
||||
export type Action = AlertmanagerAction | AlertSourceAction;
|
||||
|
||||
export type Ability = [actionSupported: boolean, actionAllowed: boolean];
|
||||
export type Abilities<T extends Action> = Record<T, Ability>;
|
||||
|
||||
export function useAlertSourceAbilities(): Abilities<AlertSourceAction> {
|
||||
// TODO add the "supported" booleans here, we currently only do authorization
|
||||
|
||||
const abilities: Abilities<AlertSourceAction> = {
|
||||
// -- Grafana managed alert rules --
|
||||
[AlertSourceAction.CreateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleCreate)],
|
||||
[AlertSourceAction.ViewAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleRead)],
|
||||
[AlertSourceAction.UpdateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleUpdate)],
|
||||
[AlertSourceAction.DeleteAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleDelete)],
|
||||
// -- External alert rules (Mimir / Loki / etc) --
|
||||
// for these we only have "read" and "write" permissions
|
||||
[AlertSourceAction.CreateExternalAlertRule]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
|
||||
],
|
||||
[AlertSourceAction.ViewExternalAlertRule]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(AccessControlAction.AlertingRuleExternalRead),
|
||||
],
|
||||
[AlertSourceAction.UpdateExternalAlertRule]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
|
||||
],
|
||||
[AlertSourceAction.DeleteExternalAlertRule]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
|
||||
],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
}
|
||||
|
||||
export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
const {
|
||||
selectedAlertmanager,
|
||||
hasConfigurationAPI,
|
||||
isGrafanaAlertmanager: isGrafanaFlavoredAlertmanager,
|
||||
} = useAlertmanager();
|
||||
|
||||
// These are used for interacting with Alertmanager resources where we apply alert.notifications:<name> permissions.
|
||||
// There are different permissions based on wether the built-in alertmanager is selected (grafana) or an external one.
|
||||
const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!);
|
||||
const instancePermissions = getInstancesPermissions(selectedAlertmanager!);
|
||||
|
||||
// list out all of the abilities, and if the user has permissions to perform them
|
||||
const abilities: Abilities<AlertmanagerAction> = {
|
||||
// -- configuration --
|
||||
[AlertmanagerAction.ViewExternalConfiguration]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalRead),
|
||||
],
|
||||
[AlertmanagerAction.UpdateExternalConfiguration]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalWrite),
|
||||
],
|
||||
// -- contact points --
|
||||
[AlertmanagerAction.CreateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
|
||||
[AlertmanagerAction.ViewContactPoint]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
|
||||
[AlertmanagerAction.UpdateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
|
||||
[AlertmanagerAction.DeleteContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
|
||||
// only Grafana flavored alertmanager supports exporting
|
||||
[AlertmanagerAction.ExportContactPoint]: [
|
||||
isGrafanaFlavoredAlertmanager,
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.read) ||
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||
],
|
||||
// -- notification templates --
|
||||
[AlertmanagerAction.CreateNotificationTemplate]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.create),
|
||||
],
|
||||
[AlertmanagerAction.ViewNotificationTemplate]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
|
||||
[AlertmanagerAction.UpdateNotificationTemplate]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.update),
|
||||
],
|
||||
[AlertmanagerAction.DeleteNotificationTemplate]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.delete),
|
||||
],
|
||||
// -- notification policies --
|
||||
[AlertmanagerAction.CreateNotificationPolicy]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.create),
|
||||
],
|
||||
[AlertmanagerAction.ViewNotificationPolicyTree]: [
|
||||
AlwaysSupported,
|
||||
ctx.hasPermission(notificationsPermissions.read),
|
||||
],
|
||||
[AlertmanagerAction.UpdateNotificationPolicyTree]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.update),
|
||||
],
|
||||
[AlertmanagerAction.DeleteNotificationPolicy]: [
|
||||
hasConfigurationAPI,
|
||||
ctx.hasPermission(notificationsPermissions.delete),
|
||||
],
|
||||
[AlertmanagerAction.ExportNotificationPolicies]: [
|
||||
isGrafanaFlavoredAlertmanager,
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.read) ||
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||
],
|
||||
// -- silences --
|
||||
[AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
|
||||
[AlertmanagerAction.ViewSilence]: [AlwaysSupported, ctx.hasPermission(instancePermissions.read)],
|
||||
[AlertmanagerAction.UpdateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.update)],
|
||||
// -- mute timtings --
|
||||
[AlertmanagerAction.CreateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
|
||||
[AlertmanagerAction.ViewMuteTiming]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
|
||||
[AlertmanagerAction.UpdateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
|
||||
[AlertmanagerAction.DeleteMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
|
||||
};
|
||||
|
||||
return abilities;
|
||||
}
|
||||
|
||||
export function useAlertmanagerAbility(action: AlertmanagerAction): Ability {
|
||||
const abilities = useAllAlertmanagerAbilities();
|
||||
|
||||
return useMemo(() => {
|
||||
return abilities[action];
|
||||
}, [abilities, action]);
|
||||
}
|
||||
|
||||
export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability[] {
|
||||
const abilities = useAllAlertmanagerAbilities();
|
||||
|
||||
return useMemo(() => {
|
||||
return actions.map((action) => abilities[action]);
|
||||
}, [abilities, actions]);
|
||||
}
|
||||
|
||||
export function useAlertSourceAbility(action: AlertSourceAction): Ability {
|
||||
const abilities = useAlertSourceAbilities();
|
||||
return useMemo(() => abilities[action], [abilities, action]);
|
||||
}
|
@ -4,12 +4,13 @@ import React from 'react';
|
||||
import { MemoryRouter, Router } from 'react-router-dom';
|
||||
|
||||
import store from 'app/core/store';
|
||||
import { AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import * as useAlertManagerSources from '../hooks/useAlertManagerSources';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
|
||||
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
import { AlertmanagerProvider, useAlertmanager } from './AlertmanagerContext';
|
||||
import { AlertmanagerProvider, isAlertManagerWithConfigAPI, useAlertmanager } from './AlertmanagerContext';
|
||||
|
||||
const grafanaAm: AlertManagerDataSource = {
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
@ -118,3 +119,23 @@ describe('useAlertmanager', () => {
|
||||
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
|
||||
});
|
||||
});
|
||||
|
||||
test('isAlertManagerWithConfigAPI', () => {
|
||||
expect(
|
||||
isAlertManagerWithConfigAPI({
|
||||
implementation: undefined,
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isAlertManagerWithConfigAPI({
|
||||
implementation: AlertManagerImplementation.mimir,
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isAlertManagerWithConfigAPI({
|
||||
implementation: AlertManagerImplementation.prometheus,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
@ -2,13 +2,21 @@ import * as React from 'react';
|
||||
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import store from 'app/core/store';
|
||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
|
||||
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import {
|
||||
AlertManagerDataSource,
|
||||
getAlertmanagerDataSourceByName,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
} from '../utils/datasource';
|
||||
|
||||
interface Context {
|
||||
selectedAlertmanager: string | undefined;
|
||||
hasConfigurationAPI: boolean; // returns true when a configuration API is available
|
||||
isGrafanaAlertmanager: boolean; // returns true if we are dealing with the built-in Alertmanager
|
||||
selectedAlertmanagerConfig: AlertManagerDataSourceJsonData | undefined;
|
||||
availableAlertManagers: AlertManagerDataSource[];
|
||||
setSelectedAlertmanager: (name: string) => void;
|
||||
}
|
||||
@ -17,9 +25,11 @@ const AlertmanagerContext = React.createContext<Context | undefined>(undefined);
|
||||
|
||||
interface Props extends React.PropsWithChildren {
|
||||
accessType: 'instance' | 'notification';
|
||||
// manually setting the alertmanagersource name will override all of the other sources
|
||||
alertmanagerSourceName?: string;
|
||||
}
|
||||
|
||||
const AlertmanagerProvider = ({ children, accessType }: Props) => {
|
||||
const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }: Props) => {
|
||||
const [queryParams, updateQueryParams] = useQueryParams();
|
||||
const availableAlertManagers = useAlertManagersByPermission(accessType);
|
||||
|
||||
@ -45,13 +55,26 @@ const AlertmanagerProvider = ({ children, accessType }: Props) => {
|
||||
const defaultSource = GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
// queryParam > localStorage > default
|
||||
const desiredAlertmanager = sourceFromQuery ?? sourceFromStore ?? defaultSource;
|
||||
const desiredAlertmanager = alertmanagerSourceName ?? sourceFromQuery ?? sourceFromStore ?? defaultSource;
|
||||
const selectedAlertmanager = isAlertManagerAvailable(availableAlertManagers, desiredAlertmanager)
|
||||
? desiredAlertmanager
|
||||
: undefined;
|
||||
|
||||
const selectedAlertmanagerConfig = getAlertmanagerDataSourceByName(selectedAlertmanager)?.jsonData;
|
||||
|
||||
// determine if we're dealing with an Alertmanager data source that supports the ruler API
|
||||
const isGrafanaAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
const isAlertmanagerWithConfigAPI = selectedAlertmanagerConfig
|
||||
? isAlertManagerWithConfigAPI(selectedAlertmanagerConfig)
|
||||
: false;
|
||||
|
||||
const hasConfigurationAPI = isGrafanaAlertmanager || isAlertmanagerWithConfigAPI;
|
||||
|
||||
const value: Context = {
|
||||
selectedAlertmanager,
|
||||
hasConfigurationAPI,
|
||||
isGrafanaAlertmanager,
|
||||
selectedAlertmanagerConfig,
|
||||
availableAlertManagers,
|
||||
setSelectedAlertmanager: updateSelectedAlertmanager,
|
||||
};
|
||||
@ -75,3 +98,15 @@ function isAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[
|
||||
const availableAlertManagersNames = availableAlertManagers.map((am) => am.name);
|
||||
return availableAlertManagersNames.includes(alertManagerName);
|
||||
}
|
||||
|
||||
// when the `implementation` is set to `undefined` we assume we're dealing with an AlertManager with config API. The reason for this is because
|
||||
// our Hosted Grafana stacks provision Alertmanager data sources without `jsonData: { implementation: "mimir" }`.
|
||||
const CONFIG_API_ENABLED_ALERTMANAGER_FLAVORS = [
|
||||
AlertManagerImplementation.mimir,
|
||||
AlertManagerImplementation.cortex,
|
||||
undefined,
|
||||
];
|
||||
|
||||
export function isAlertManagerWithConfigAPI(dataSourceConfig: AlertManagerDataSourceJsonData): boolean {
|
||||
return CONFIG_API_ENABLED_ALERTMANAGER_FLAVORS.includes(dataSourceConfig.implementation);
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { isValidPrometheusDuration, parsePrometheusDuration } from './time';
|
||||
|
||||
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||
export function getAllDataSources(): Array<
|
||||
DataSourceInstanceSettings<DataSourceJsonData | AlertManagerDataSourceJsonData>
|
||||
> {
|
||||
return Object.values(config.datasources);
|
||||
}
|
||||
|
||||
|
@ -137,8 +137,7 @@ export function isCloudRulesSource(rulesSource: RulesSource | string): rulesSour
|
||||
export function isVanillaPrometheusAlertManagerDataSource(name: string): boolean {
|
||||
return (
|
||||
name !== GRAFANA_RULES_SOURCE_NAME &&
|
||||
(getDataSourceByName(name)?.jsonData as AlertManagerDataSourceJsonData)?.implementation ===
|
||||
AlertManagerImplementation.prometheus
|
||||
getAlertmanagerDataSourceByName(name)?.jsonData?.implementation === AlertManagerImplementation.prometheus
|
||||
);
|
||||
}
|
||||
|
||||
@ -152,6 +151,13 @@ export function getDataSourceByName(name: string): DataSourceInstanceSettings<Da
|
||||
return getAllDataSources().find((source) => source.name === name);
|
||||
}
|
||||
|
||||
export function getAlertmanagerDataSourceByName(name: string) {
|
||||
return getAllDataSources().find(
|
||||
(source): source is DataSourceInstanceSettings<AlertManagerDataSourceJsonData> =>
|
||||
source.name === name && source.type === 'alertmanager'
|
||||
);
|
||||
}
|
||||
|
||||
export function getRulesSourceByName(name: string): RulesSource | undefined {
|
||||
if (name === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return GRAFANA_RULES_SOURCE_NAME;
|
||||
|
Loading…
Reference in New Issue
Block a user