mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: fgac for notification policies and contact points (#46939)
* add FGAC actions for silences table * redirect users without permissions * add permissions checks to routes * add fgac to notifications and contact points * fgac for notification policies * fix mute timing authorization * use consistent naming for checking grafana alertmanager * tests for fgac in contact points and notification policies * bump up timeout on rule editor test * use new permissions util * break out route evaluation into util * Remove test timeout * Change permissions for the alert-notifiers endpoint * Use signed in handler for alert-notifiers when unified alerting enabled Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
var plog = log.New("api")
|
var plog = log.New("api")
|
||||||
@@ -417,7 +418,14 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
|
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
|
||||||
})
|
})
|
||||||
|
|
||||||
apiRoute.Get("/alert-notifiers", reqEditorRole, routing.Wrap(
|
var notifiersAuthHandler web.Handler
|
||||||
|
if hs.Cfg.UnifiedAlerting.IsEnabled() {
|
||||||
|
notifiersAuthHandler = reqSignedIn
|
||||||
|
} else {
|
||||||
|
notifiersAuthHandler = reqEditorRole
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap(
|
||||||
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
|
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
|
|||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { evaluateAccess } from './unified/utils/access-control';
|
||||||
|
|
||||||
const commonRoutes: RouteDescriptor[] = [
|
const commonRoutes: RouteDescriptor[] = [
|
||||||
{
|
{
|
||||||
@@ -95,84 +96,118 @@ const unifiedRoutes: RouteDescriptor[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/routes',
|
path: '/alerting/routes',
|
||||||
roles: () => ['Admin', 'Editor'],
|
roles: () =>
|
||||||
|
contextSrv.evaluatePermission(config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : () => [], [
|
||||||
|
AccessControlAction.AlertingNotificationsRead,
|
||||||
|
AccessControlAction.AlertingNotificationsExternalRead,
|
||||||
|
]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/routes/mute-timing/new',
|
path: '/alerting/routes/mute-timing/new',
|
||||||
roles: () => ['Admin', 'Editor'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
|
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/routes/mute-timing/edit',
|
path: '/alerting/routes/mute-timing/edit',
|
||||||
roles: () => ['Admin', 'Editor'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
|
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silences',
|
path: '/alerting/silences',
|
||||||
roles: () => contextSrv.evaluatePermission(() => [], [AccessControlAction.AlertingInstanceRead]),
|
roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silence/new',
|
path: '/alerting/silence/new',
|
||||||
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceCreate]),
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstancesExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silence/:id/edit',
|
path: '/alerting/silence/:id/edit',
|
||||||
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceUpdate]),
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingInstanceUpdate, AccessControlAction.AlertingInstancesExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications',
|
path: '/alerting/notifications',
|
||||||
roles: config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : undefined,
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/templates/new',
|
path: '/alerting/notifications/templates/new',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/templates/:id/edit',
|
path: '/alerting/notifications/templates/:id/edit',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/receivers/new',
|
path: '/alerting/notifications/receivers/new',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/receivers/:id/edit',
|
path: '/alerting/notifications/receivers/:id/edit',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/global-config',
|
path: '/alerting/notifications/global-config',
|
||||||
roles: () => ['Admin', 'Editor'],
|
roles: evaluateAccess(
|
||||||
|
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||||
|
['Editor', 'Admin']
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { selectOptionInTest } from '@grafana/ui';
|
import { selectOptionInTest } from '@grafana/ui';
|
||||||
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
|
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
jest.mock('./api/alertmanager');
|
jest.mock('./api/alertmanager');
|
||||||
jest.mock('./utils/config');
|
jest.mock('./utils/config');
|
||||||
|
jest.mock('app/core/services/context_srv');
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
getAllDataSourcesMock: jest.mocked(getAllDataSources),
|
getAllDataSourcesMock: jest.mocked(getAllDataSources),
|
||||||
@@ -32,6 +35,7 @@ const mocks = {
|
|||||||
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
|
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
|
||||||
fetchStatus: jest.mocked(fetchStatus),
|
fetchStatus: jest.mocked(fetchStatus),
|
||||||
},
|
},
|
||||||
|
contextSrv: jest.mocked(contextSrv),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAmRoutes = (alertManagerSourceName?: string) => {
|
const renderAmRoutes = (alertManagerSourceName?: string) => {
|
||||||
@@ -177,6 +181,9 @@ describe('AmRoutes', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||||
|
mocks.contextSrv.hasPermission.mockImplementation(() => true);
|
||||||
|
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -359,6 +366,18 @@ describe('AmRoutes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides create and edit button if user does not have permission', () => {
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation((action) =>
|
||||||
|
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsRead].includes(
|
||||||
|
action as AccessControlAction
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAmRoutes();
|
||||||
|
expect(ui.newPolicyButton.query()).not.toBeInTheDocument();
|
||||||
|
expect(ui.editButton.query()).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('Show error message if loading Alertmanager config fails', async () => {
|
it('Show error message if loading Alertmanager config fails', async () => {
|
||||||
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
|
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
|
||||||
status: 500,
|
status: 500,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const AmRoutes: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div className={styles.break} />
|
<div className={styles.break} />
|
||||||
<AmSpecificRouting
|
<AmSpecificRouting
|
||||||
|
alertManagerSourceName={alertManagerSourceName}
|
||||||
onChange={handleSave}
|
onChange={handleSave}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onRootRouteEdit={enterRootRouteEditMode}
|
onRootRouteEdit={enterRootRouteEditMode}
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ import { contextSrv } from 'app/core/services/context_srv';
|
|||||||
import { selectOptionInTest } from '@grafana/ui';
|
import { selectOptionInTest } from '@grafana/ui';
|
||||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
|
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
jest.mock('./api/alertmanager');
|
jest.mock('./api/alertmanager');
|
||||||
jest.mock('./api/grafana');
|
jest.mock('./api/grafana');
|
||||||
jest.mock('./utils/config');
|
jest.mock('./utils/config');
|
||||||
|
jest.mock('app/core/services/context_srv');
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
getAllDataSources: jest.mocked(getAllDataSources),
|
getAllDataSources: jest.mocked(getAllDataSources),
|
||||||
@@ -40,6 +42,7 @@ const mocks = {
|
|||||||
fetchNotifiers: jest.mocked(fetchNotifiers),
|
fetchNotifiers: jest.mocked(fetchNotifiers),
|
||||||
testReceivers: jest.mocked(testReceivers),
|
testReceivers: jest.mocked(testReceivers),
|
||||||
},
|
},
|
||||||
|
contextSrv: jest.mocked(contextSrv),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReceivers = (alertManagerSourceName?: string) => {
|
const renderReceivers = (alertManagerSourceName?: string) => {
|
||||||
@@ -125,8 +128,23 @@ describe('Receivers', () => {
|
|||||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||||
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
|
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||||
contextSrv.isEditor = true;
|
mocks.contextSrv.isEditor = true;
|
||||||
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
|
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
|
||||||
|
mocks.contextSrv.hasPermission.mockImplementation((action) => {
|
||||||
|
const permissions = [
|
||||||
|
AccessControlAction.AlertingNotificationsRead,
|
||||||
|
AccessControlAction.AlertingNotificationsCreate,
|
||||||
|
AccessControlAction.AlertingNotificationsUpdate,
|
||||||
|
AccessControlAction.AlertingNotificationsDelete,
|
||||||
|
AccessControlAction.AlertingNotificationsExternalRead,
|
||||||
|
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||||
|
];
|
||||||
|
return permissions.includes(action as AccessControlAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
|
it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
|
||||||
@@ -295,6 +313,19 @@ describe('Receivers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Hides create contact point button for users without permission', () => {
|
||||||
|
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
|
||||||
|
mocks.api.updateConfig.mockResolvedValue();
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation((action) =>
|
||||||
|
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some(
|
||||||
|
(a) => a === action
|
||||||
|
)
|
||||||
|
);
|
||||||
|
renderReceivers();
|
||||||
|
|
||||||
|
expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('Cloud alertmanager receiver can be edited', async () => {
|
it('Cloud alertmanager receiver can be edited', async () => {
|
||||||
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
|
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
|
||||||
mocks.api.updateConfig.mockResolvedValue();
|
mocks.api.updateConfig.mockResolvedValue();
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ const Receivers: FC = () => {
|
|||||||
}, [alertManagerSourceName, dispatch, shouldLoadConfig]);
|
}, [alertManagerSourceName, dispatch, shouldLoadConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && !(receiverTypes.result || receiverTypes.loading)) {
|
if (
|
||||||
|
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
|
||||||
|
!(receiverTypes.result || receiverTypes.loading || receiverTypes.error)
|
||||||
|
) {
|
||||||
dispatch(fetchGrafanaNotifiersAction());
|
dispatch(fetchGrafanaNotifiersAction());
|
||||||
}
|
}
|
||||||
}, [alertManagerSourceName, dispatch, receiverTypes]);
|
}, [alertManagerSourceName, dispatch, receiverTypes]);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface EmptyAreaWithCTAProps {
|
|||||||
buttonIcon?: IconName;
|
buttonIcon?: IconName;
|
||||||
buttonSize?: 'xs' | 'sm' | 'md' | 'lg';
|
buttonSize?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
buttonVariant?: ButtonVariant;
|
buttonVariant?: ButtonVariant;
|
||||||
|
showButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
||||||
@@ -23,6 +24,7 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
|||||||
onButtonClick,
|
onButtonClick,
|
||||||
text,
|
text,
|
||||||
href,
|
href,
|
||||||
|
showButton = true,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
@@ -37,15 +39,16 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
|||||||
<EmptyArea>
|
<EmptyArea>
|
||||||
<>
|
<>
|
||||||
<p className={styles.text}>{text}</p>
|
<p className={styles.text}>{text}</p>
|
||||||
{href ? (
|
{showButton &&
|
||||||
<LinkButton href={href} type="button" {...commonProps}>
|
(href ? (
|
||||||
{buttonLabel}
|
<LinkButton href={href} type="button" {...commonProps}>
|
||||||
</LinkButton>
|
{buttonLabel}
|
||||||
) : (
|
</LinkButton>
|
||||||
<Button onClick={onButtonClick} type="button" {...commonProps}>
|
) : (
|
||||||
{buttonLabel}
|
<Button onClick={onButtonClick} type="button" {...commonProps}>
|
||||||
</Button>
|
{buttonLabel}
|
||||||
)}
|
</Button>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
</EmptyArea>
|
</EmptyArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { AccessControlAction } from 'app/types';
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { isGrafanaRulesSource } from '../../utils/datasource';
|
|
||||||
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
||||||
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
|
import { getInstancesPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
interface AmNotificationsAlertDetailsProps {
|
interface AmNotificationsAlertDetailsProps {
|
||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
@@ -17,18 +16,11 @@ interface AmNotificationsAlertDetailsProps {
|
|||||||
|
|
||||||
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.actionsRow}>
|
<div className={styles.actionsRow}>
|
||||||
<Authorize
|
<Authorize actions={[permissions.update, permissions.create]} fallback={contextSrv.isEditor}>
|
||||||
actions={
|
|
||||||
isExternalAM
|
|
||||||
? [AccessControlAction.AlertingInstancesExternalWrite]
|
|
||||||
: [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstanceUpdate]
|
|
||||||
}
|
|
||||||
fallback={contextSrv.isEditor}
|
|
||||||
>
|
|
||||||
{alert.status.state === AlertState.Suppressed && (
|
{alert.status.state === AlertState.Suppressed && (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href={`${makeAMLink(
|
href={`${makeAMLink(
|
||||||
@@ -53,9 +45,7 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
|
|||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
</Authorize>
|
</Authorize>
|
||||||
<Authorize
|
<Authorize actions={[permissions.viewSource]}>
|
||||||
actions={isExternalAM ? [AccessControlAction.DataSourcesExplore] : [AccessControlAction.AlertingInstanceRead]}
|
|
||||||
>
|
|
||||||
{alert.generatorURL && (
|
{alert.generatorURL && (
|
||||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||||
See source
|
See source
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
|||||||
import { AmRootRouteForm } from './AmRootRouteForm';
|
import { AmRootRouteForm } from './AmRootRouteForm';
|
||||||
import { AmRootRouteRead } from './AmRootRouteRead';
|
import { AmRootRouteRead } from './AmRootRouteRead';
|
||||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||||
|
import { Authorize } from '../../components/Authorize';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
export interface AmRootRouteProps {
|
export interface AmRootRouteProps {
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
onEnterEditMode: () => void;
|
onEnterEditMode: () => void;
|
||||||
@@ -28,6 +29,7 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||||
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,9 +39,11 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
|||||||
Root policy - <i>default for all alerts</i>
|
Root policy - <i>default for all alerts</i>
|
||||||
</h5>
|
</h5>
|
||||||
{!isEditMode && !isReadOnly && (
|
{!isEditMode && !isReadOnly && (
|
||||||
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
|
<Authorize actions={[permissions.update]}>
|
||||||
Edit
|
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
|
||||||
</Button>
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Authorize>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import { emptyRoute } from '../../utils/amroutes';
|
|||||||
import { AmRoutesTable } from './AmRoutesTable';
|
import { AmRoutesTable } from './AmRoutesTable';
|
||||||
import { getGridStyles } from './gridStyles';
|
import { getGridStyles } from './gridStyles';
|
||||||
import { MuteTimingsTable } from './MuteTimingsTable';
|
import { MuteTimingsTable } from './MuteTimingsTable';
|
||||||
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
|
import { Authorize } from '../Authorize';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
export interface AmRoutesExpandedReadProps {
|
export interface AmRoutesExpandedReadProps {
|
||||||
onChange: (routes: FormAmRoute) => void;
|
onChange: (routes: FormAmRoute) => void;
|
||||||
receivers: AmRouteReceiver[];
|
receivers: AmRouteReceiver[];
|
||||||
routes: FormAmRoute;
|
routes: FormAmRoute;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
alertManagerSourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
|
export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
|
||||||
@@ -21,10 +23,11 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
|
|||||||
receivers,
|
receivers,
|
||||||
routes,
|
routes,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
alertManagerSourceName,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const gridStyles = useStyles2(getGridStyles);
|
const gridStyles = useStyles2(getGridStyles);
|
||||||
const [alertManagerSourceName] = useAlertManagerSourceName();
|
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||||
|
|
||||||
const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-';
|
const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-';
|
||||||
const groupInterval = routes.groupIntervalValue
|
const groupInterval = routes.groupIntervalValue
|
||||||
@@ -71,23 +74,26 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
|
|||||||
}}
|
}}
|
||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={subroutes}
|
routes={subroutes}
|
||||||
|
alertManagerSourceName={alertManagerSourceName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p>No nested policies configured.</p>
|
<p>No nested policies configured.</p>
|
||||||
)}
|
)}
|
||||||
{!isAddMode && !readOnly && (
|
{!isAddMode && !readOnly && (
|
||||||
<Button
|
<Authorize actions={[permissions.create]}>
|
||||||
className={styles.addNestedRoutingBtn}
|
<Button
|
||||||
icon="plus"
|
className={styles.addNestedRoutingBtn}
|
||||||
onClick={() => {
|
icon="plus"
|
||||||
setSubroutes((subroutes) => [...subroutes, emptyRoute]);
|
onClick={() => {
|
||||||
setIsAddMode(true);
|
setSubroutes((subroutes) => [...subroutes, emptyRoute]);
|
||||||
}}
|
setIsAddMode(true);
|
||||||
variant="secondary"
|
}}
|
||||||
type="button"
|
variant="secondary"
|
||||||
>
|
type="button"
|
||||||
Add nested policy
|
>
|
||||||
</Button>
|
Add nested policy
|
||||||
|
</Button>
|
||||||
|
</Authorize>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={gridStyles.titleCell}>Mute timings</div>
|
<div className={gridStyles.titleCell}>Mute timings</div>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { Matchers } from '../silences/Matchers';
|
|||||||
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
|
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
|
||||||
import { intersectionWith, isEqual } from 'lodash';
|
import { intersectionWith, isEqual } from 'lodash';
|
||||||
import { EmptyArea } from '../EmptyArea';
|
import { EmptyArea } from '../EmptyArea';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
export interface AmRoutesTableProps {
|
export interface AmRoutesTableProps {
|
||||||
isAddMode: boolean;
|
isAddMode: boolean;
|
||||||
@@ -18,6 +20,7 @@ export interface AmRoutesTableProps {
|
|||||||
routes: FormAmRoute[];
|
routes: FormAmRoute[];
|
||||||
filters?: { queryString?: string; contactPoint?: string };
|
filters?: { queryString?: string; contactPoint?: string };
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
alertManagerSourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
|
type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
|
||||||
@@ -69,9 +72,15 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
routes,
|
routes,
|
||||||
filters,
|
filters,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
alertManagerSourceName,
|
||||||
}) => {
|
}) => {
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [expandedId, setExpandedId] = useState<string | number>();
|
const [expandedId, setExpandedId] = useState<string | number>();
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||||
|
const canEditRoutes = contextSrv.hasPermission(permissions.update);
|
||||||
|
const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
|
||||||
|
|
||||||
|
const showActions = !readOnly && (canEditRoutes || canDeleteRoutes);
|
||||||
|
|
||||||
const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
|
const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
|
||||||
const collapseItem = useCallback(() => setExpandedId(undefined), []);
|
const collapseItem = useCallback(() => setExpandedId(undefined), []);
|
||||||
@@ -102,7 +111,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
|
renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
|
||||||
size: 5,
|
size: 5,
|
||||||
},
|
},
|
||||||
...(readOnly
|
...(!showActions
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
@@ -212,6 +221,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={item.data}
|
routes={item.data}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
alertManagerSourceName={alertManagerSourceName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ import { MatcherFilter } from '../alert-groups/MatcherFilter';
|
|||||||
import { EmptyArea } from '../EmptyArea';
|
import { EmptyArea } from '../EmptyArea';
|
||||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||||
import { AmRoutesTable } from './AmRoutesTable';
|
import { AmRoutesTable } from './AmRoutesTable';
|
||||||
|
import { Authorize } from '../../components/Authorize';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
export interface AmSpecificRoutingProps {
|
export interface AmSpecificRoutingProps {
|
||||||
|
alertManagerSourceName: string;
|
||||||
onChange: (routes: FormAmRoute) => void;
|
onChange: (routes: FormAmRoute) => void;
|
||||||
onRootRouteEdit: () => void;
|
onRootRouteEdit: () => void;
|
||||||
receivers: AmRouteReceiver[];
|
receivers: AmRouteReceiver[];
|
||||||
@@ -26,6 +30,7 @@ interface Filters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
||||||
|
alertManagerSourceName,
|
||||||
onChange,
|
onChange,
|
||||||
onRootRouteEdit,
|
onRootRouteEdit,
|
||||||
receivers,
|
receivers,
|
||||||
@@ -34,6 +39,8 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
|
const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
|
||||||
const [isAddMode, setIsAddMode] = useState(false);
|
const [isAddMode, setIsAddMode] = useState(false);
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||||
|
const canCreateNotifications = contextSrv.hasPermission(permissions.create);
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||||
const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
|
const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
|
||||||
@@ -97,6 +104,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
buttonLabel="Set a default contact point"
|
buttonLabel="Set a default contact point"
|
||||||
onButtonClick={onRootRouteEdit}
|
onButtonClick={onRootRouteEdit}
|
||||||
text="You haven't set a default contact point for the root route yet."
|
text="You haven't set a default contact point for the root route yet."
|
||||||
|
showButton={canCreateNotifications}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : actualRoutes.length > 0 ? (
|
) : actualRoutes.length > 0 ? (
|
||||||
@@ -132,11 +140,13 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isAddMode && !readOnly && (
|
{!isAddMode && !readOnly && (
|
||||||
<div className={styles.addMatcherBtnRow}>
|
<Authorize actions={[permissions.create]}>
|
||||||
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
<div className={styles.addMatcherBtnRow}>
|
||||||
New policy
|
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
||||||
</Button>
|
New policy
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Authorize>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AmRoutesTable
|
<AmRoutesTable
|
||||||
@@ -147,6 +157,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={actualRoutes}
|
routes={actualRoutes}
|
||||||
filters={{ queryString, contactPoint }}
|
filters={{ queryString, contactPoint }}
|
||||||
|
alertManagerSourceName={alertManagerSourceName}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : readOnly ? (
|
) : readOnly ? (
|
||||||
@@ -159,6 +170,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
buttonLabel="New specific policy"
|
buttonLabel="New specific policy"
|
||||||
onButtonClick={addNewRoute}
|
onButtonClick={addNewRoute}
|
||||||
text="You haven't created any specific policies yet."
|
text="You haven't created any specific policies yet."
|
||||||
|
showButton={canCreateNotifications}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import {
|
|||||||
getYearsString,
|
getYearsString,
|
||||||
} from '../../utils/alertmanager';
|
} from '../../utils/alertmanager';
|
||||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||||
|
import { Authorize } from '../../components/Authorize';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
@@ -27,6 +30,7 @@ interface Props {
|
|||||||
export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTimingNames, hideActions }) => {
|
export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTimingNames, hideActions }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||||
const [muteTimingName, setMuteTimingName] = useState<string>('');
|
const [muteTimingName, setMuteTimingName] = useState<string>('');
|
||||||
const { result }: AsyncRequestState<AlertManagerCortexConfig> =
|
const { result }: AsyncRequestState<AlertManagerCortexConfig> =
|
||||||
@@ -56,14 +60,16 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!hideActions && items.length > 0 && (
|
{!hideActions && items.length > 0 && (
|
||||||
<LinkButton
|
<Authorize actions={[permissions.create]}>
|
||||||
className={styles.addMuteButton}
|
<LinkButton
|
||||||
icon="plus"
|
className={styles.addMuteButton}
|
||||||
variant="primary"
|
icon="plus"
|
||||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
variant="primary"
|
||||||
>
|
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||||
New mute timing
|
>
|
||||||
</LinkButton>
|
New mute timing
|
||||||
|
</LinkButton>
|
||||||
|
</Authorize>
|
||||||
)}
|
)}
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<DynamicTable items={items} cols={columns} />
|
<DynamicTable items={items} cols={columns} />
|
||||||
@@ -74,6 +80,7 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
|||||||
buttonIcon="plus"
|
buttonIcon="plus"
|
||||||
buttonSize="lg"
|
buttonSize="lg"
|
||||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||||
|
showButton={contextSrv.hasPermission(permissions.create)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p>No mute timings configured</p>
|
<p>No mute timings configured</p>
|
||||||
@@ -93,6 +100,11 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
|||||||
};
|
};
|
||||||
|
|
||||||
function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) {
|
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);
|
||||||
return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
|
return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
|
||||||
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
||||||
{
|
{
|
||||||
@@ -109,19 +121,29 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
|||||||
renderCell: ({ data }) => renderTimeIntervals(data.time_intervals),
|
renderCell: ({ data }) => renderTimeIntervals(data.time_intervals),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (!hideActions) {
|
if (showActions) {
|
||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
label: 'Actions',
|
label: 'Actions',
|
||||||
renderCell: function renderActions({ data }) {
|
renderCell: function renderActions({ data }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Authorize actions={[permissions.update]}>
|
||||||
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, { muteName: data.name })}
|
<Link
|
||||||
>
|
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
|
||||||
<IconButton name="edit" title="Edit mute timing" />
|
muteName: data.name,
|
||||||
</Link>
|
})}
|
||||||
<IconButton name={'trash-alt'} title="Delete mute timing" onClick={() => setMuteTimingName(data.name)} />
|
>
|
||||||
|
<IconButton name="edit" title="Edit mute timing" />
|
||||||
|
</Link>
|
||||||
|
</Authorize>
|
||||||
|
<Authorize actions={[permissions.delete]}>
|
||||||
|
<IconButton
|
||||||
|
name={'trash-alt'}
|
||||||
|
title="Delete mute timing"
|
||||||
|
onClick={() => setMuteTimingName(data.name)}
|
||||||
|
/>
|
||||||
|
</Authorize>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -129,7 +151,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}, [alertManagerSourceName, hideActions, setMuteTimingName]);
|
}, [alertManagerSourceName, setMuteTimingName, showActions, permissions]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTimeIntervals(timeIntervals: TimeInterval[]) {
|
function renderTimeIntervals(timeIntervals: TimeInterval[]) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { css } from '@emotion/css';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
|
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
|
import { Authorize } from '../Authorize';
|
||||||
import { ReceiversTable } from './ReceiversTable';
|
import { ReceiversTable } from './ReceiversTable';
|
||||||
import { TemplatesTable } from './TemplatesTable';
|
import { TemplatesTable } from './TemplatesTable';
|
||||||
|
|
||||||
@@ -22,15 +24,17 @@ export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName
|
|||||||
{!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />}
|
{!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />}
|
||||||
<ReceiversTable config={config} alertManagerName={alertManagerName} />
|
<ReceiversTable config={config} alertManagerName={alertManagerName} />
|
||||||
{isCloud && (
|
{isCloud && (
|
||||||
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
<Authorize actions={[AccessControlAction.AlertingNotificationsExternalWrite]}>
|
||||||
<p>
|
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
||||||
For each external Alertmanager you can define global settings, like server addresses, usernames and
|
<p>
|
||||||
password, for all the supported contact points.
|
For each external Alertmanager you can define global settings, like server addresses, usernames and
|
||||||
</p>
|
password, for all the supported contact points.
|
||||||
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
</p>
|
||||||
{isVanillaAM ? 'View global config' : 'Edit global config'}
|
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
||||||
</LinkButton>
|
{isVanillaAM ? 'View global config' : 'Edit global config'}
|
||||||
</Alert>
|
</LinkButton>
|
||||||
|
</Alert>
|
||||||
|
</Authorize>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { isReceiverUsed } from '../../utils/alertmanager';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { deleteReceiverAction } from '../../state/actions';
|
import { deleteReceiverAction } from '../../state/actions';
|
||||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||||
|
import { Authorize } from '../../components/Authorize';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config: AlertManagerCortexConfig;
|
config: AlertManagerCortexConfig;
|
||||||
@@ -24,6 +27,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
const tableStyles = useStyles2(getAlertTableStyles);
|
const tableStyles = useStyles2(getAlertTableStyles);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
|
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerName);
|
||||||
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
|
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
|
||||||
|
|
||||||
// receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted
|
// receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted
|
||||||
@@ -66,7 +70,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
className={styles.section}
|
className={styles.section}
|
||||||
title="Contact points"
|
title="Contact points"
|
||||||
description="Define where the notifications will be sent to, for example email or Slack."
|
description="Define where the notifications will be sent to, for example email or Slack."
|
||||||
showButton={!isVanillaAM}
|
showButton={!isVanillaAM && contextSrv.hasPermission(permissions.create)}
|
||||||
addButtonLabel="New contact point"
|
addButtonLabel="New contact point"
|
||||||
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
||||||
>
|
>
|
||||||
@@ -74,13 +78,17 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
<colgroup>
|
<colgroup>
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||||
|
<col />
|
||||||
|
</Authorize>
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Contact point name</th>
|
<th>Contact point name</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Actions</th>
|
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||||
|
<th>Actions</th>
|
||||||
|
</Authorize>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -93,38 +101,46 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||||
<td>{receiver.name}</td>
|
<td>{receiver.name}</td>
|
||||||
<td>{receiver.types.join(', ')}</td>
|
<td>{receiver.types.join(', ')}</td>
|
||||||
<td className={tableStyles.actionsCell}>
|
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||||
{!isVanillaAM && (
|
<td className={tableStyles.actionsCell}>
|
||||||
<>
|
{!isVanillaAM && (
|
||||||
<ActionIcon
|
<>
|
||||||
aria-label="Edit"
|
<Authorize actions={[permissions.update]}>
|
||||||
data-testid="edit"
|
<ActionIcon
|
||||||
to={makeAMLink(
|
aria-label="Edit"
|
||||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
data-testid="edit"
|
||||||
alertManagerName
|
to={makeAMLink(
|
||||||
)}
|
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||||
tooltip="Edit contact point"
|
alertManagerName
|
||||||
icon="pen"
|
)}
|
||||||
/>
|
tooltip="Edit contact point"
|
||||||
<ActionIcon
|
icon="pen"
|
||||||
onClick={() => onClickDeleteReceiver(receiver.name)}
|
/>
|
||||||
tooltip="Delete contact point"
|
</Authorize>
|
||||||
icon="trash-alt"
|
<Authorize actions={[permissions.delete]}>
|
||||||
/>
|
<ActionIcon
|
||||||
</>
|
onClick={() => onClickDeleteReceiver(receiver.name)}
|
||||||
)}
|
tooltip="Delete contact point"
|
||||||
{isVanillaAM && (
|
icon="trash-alt"
|
||||||
<ActionIcon
|
/>
|
||||||
data-testid="view"
|
</Authorize>
|
||||||
to={makeAMLink(
|
</>
|
||||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
)}
|
||||||
alertManagerName
|
{isVanillaAM && (
|
||||||
)}
|
<Authorize actions={[permissions.update]}>
|
||||||
tooltip="View contact point"
|
<ActionIcon
|
||||||
icon="file-alt"
|
data-testid="view"
|
||||||
/>
|
to={makeAMLink(
|
||||||
)}
|
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||||
</td>
|
alertManagerName
|
||||||
|
)}
|
||||||
|
tooltip="View contact point"
|
||||||
|
icon="file-alt"
|
||||||
|
/>
|
||||||
|
</Authorize>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</Authorize>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection';
|
|||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { deleteTemplateAction } from '../../state/actions';
|
import { deleteTemplateAction } from '../../state/actions';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { Authorize } from '../../components/Authorize';
|
||||||
|
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config: AlertManagerCortexConfig;
|
config: AlertManagerCortexConfig;
|
||||||
@@ -19,6 +22,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
|
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
|
||||||
const tableStyles = useStyles2(getAlertTableStyles);
|
const tableStyles = useStyles2(getAlertTableStyles);
|
||||||
|
const permissions = getNotificationsPermissions(alertManagerName);
|
||||||
|
|
||||||
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
|
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
|
||||||
const [templateToDelete, setTemplateToDelete] = useState<string>();
|
const [templateToDelete, setTemplateToDelete] = useState<string>();
|
||||||
@@ -36,6 +40,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
description="Templates construct the messages that get sent to the contact points."
|
description="Templates construct the messages that get sent to the contact points."
|
||||||
addButtonLabel="New template"
|
addButtonLabel="New template"
|
||||||
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
|
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
|
||||||
|
showButton={contextSrv.hasPermission(permissions.create)}
|
||||||
>
|
>
|
||||||
<table className={tableStyles.table} data-testid="templates-table">
|
<table className={tableStyles.table} data-testid="templates-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -47,7 +52,9 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Template</th>
|
<th>Template</th>
|
||||||
<th>Actions</th>
|
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||||
|
<th>Actions</th>
|
||||||
|
</Authorize>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -68,17 +75,27 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{name}</td>
|
<td>{name}</td>
|
||||||
<td className={tableStyles.actionsCell}>
|
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||||
<ActionIcon
|
<td className={tableStyles.actionsCell}>
|
||||||
to={makeAMLink(
|
<Authorize actions={[permissions.update]}>
|
||||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
<ActionIcon
|
||||||
alertManagerName
|
to={makeAMLink(
|
||||||
)}
|
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||||
tooltip="edit template"
|
alertManagerName
|
||||||
icon="pen"
|
)}
|
||||||
/>
|
tooltip="edit template"
|
||||||
<ActionIcon onClick={() => setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" />
|
icon="pen"
|
||||||
</td>
|
/>
|
||||||
|
</Authorize>
|
||||||
|
<Authorize actions={[permissions.delete]}>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => setTemplateToDelete(name)}
|
||||||
|
tooltip="delete template"
|
||||||
|
icon="trash-alt"
|
||||||
|
/>
|
||||||
|
</Authorize>
|
||||||
|
</td>
|
||||||
|
</Authorize>
|
||||||
</tr>
|
</tr>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { expireSilenceAction } from '../../state/actions';
|
import { expireSilenceAction } from '../../state/actions';
|
||||||
import { SilenceDetails } from './SilenceDetails';
|
import { SilenceDetails } from './SilenceDetails';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { AccessControlAction } from '../../../../../types';
|
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
import { isGrafanaRulesSource } from '../../utils/datasource';
|
import { getInstancesPermissions } from '../../utils/access-control';
|
||||||
|
|
||||||
export interface SilenceTableItem extends Silence {
|
export interface SilenceTableItem extends Silence {
|
||||||
silencedAlerts: AlertmanagerAlert[];
|
silencedAlerts: AlertmanagerAlert[];
|
||||||
@@ -38,7 +37,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const filteredSilences = useFilteredSilences(silences);
|
const filteredSilences = useFilteredSilences(silences);
|
||||||
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||||
|
|
||||||
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
||||||
|
|
||||||
@@ -65,14 +64,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
{!!silences.length && (
|
{!!silences.length && (
|
||||||
<>
|
<>
|
||||||
<SilencesFilter />
|
<SilencesFilter />
|
||||||
<Authorize
|
<Authorize actions={[permissions.create]} fallback={contextSrv.isEditor}>
|
||||||
actions={
|
|
||||||
isExternalAM
|
|
||||||
? [AccessControlAction.AlertingInstancesExternalWrite]
|
|
||||||
: [AccessControlAction.AlertingInstanceCreate]
|
|
||||||
}
|
|
||||||
fallback={contextSrv.isEditor}
|
|
||||||
>
|
|
||||||
<div className={styles.topButtonContainer}>
|
<div className={styles.topButtonContainer}>
|
||||||
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
||||||
<Button className={styles.addNewSilence} icon="plus">
|
<Button className={styles.addNewSilence} icon="plus">
|
||||||
@@ -178,15 +170,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
function useColumns(alertManagerSourceName: string) {
|
function useColumns(alertManagerSourceName: string) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||||
return useMemo((): SilenceTableColumnProps[] => {
|
return useMemo((): SilenceTableColumnProps[] => {
|
||||||
const handleExpireSilenceClick = (id: string) => {
|
const handleExpireSilenceClick = (id: string) => {
|
||||||
dispatch(expireSilenceAction(alertManagerSourceName, id));
|
dispatch(expireSilenceAction(alertManagerSourceName, id));
|
||||||
};
|
};
|
||||||
const showActions = contextSrv.hasAccess(
|
const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
|
||||||
isExternalAM ? AccessControlAction.AlertingInstancesExternalWrite : AccessControlAction.AlertingInstanceUpdate,
|
|
||||||
contextSrv.isEditor
|
|
||||||
);
|
|
||||||
const columns: SilenceTableColumnProps[] = [
|
const columns: SilenceTableColumnProps[] = [
|
||||||
{
|
{
|
||||||
id: 'state',
|
id: 'state',
|
||||||
@@ -262,7 +251,7 @@ function useColumns(alertManagerSourceName: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}, [alertManagerSourceName, dispatch, styles, isExternalAM]);
|
}, [alertManagerSourceName, dispatch, styles, permissions]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SilencesTable;
|
export default SilencesTable;
|
||||||
|
|||||||
78
public/app/features/alerting/unified/utils/access-control.ts
Normal file
78
public/app/features/alerting/unified/utils/access-control.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { isGrafanaRulesSource } from './datasource';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
|
function getAMversion(alertManagerSourceName: string) {
|
||||||
|
return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstancesPermissions(alertManagerSourceName: string) {
|
||||||
|
const amVersion = getAMversion(alertManagerSourceName);
|
||||||
|
|
||||||
|
const permissions = {
|
||||||
|
read: {
|
||||||
|
grafana: AccessControlAction.AlertingInstanceRead,
|
||||||
|
external: AccessControlAction.AlertingInstancesExternalRead,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
grafana: AccessControlAction.AlertingInstanceCreate,
|
||||||
|
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||||
|
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||||
|
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||||
|
},
|
||||||
|
viewSource: {
|
||||||
|
grafana: AccessControlAction.AlertingInstanceRead,
|
||||||
|
external: AccessControlAction.DataSourcesExplore,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
read: permissions.read[amVersion],
|
||||||
|
create: permissions.create[amVersion],
|
||||||
|
update: permissions.update[amVersion],
|
||||||
|
delete: permissions.delete[amVersion],
|
||||||
|
viewSource: permissions.viewSource[amVersion],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotificationsPermissions(alertManagerSourceName: string) {
|
||||||
|
const amVersion = getAMversion(alertManagerSourceName);
|
||||||
|
|
||||||
|
const permissions = {
|
||||||
|
read: {
|
||||||
|
grafana: AccessControlAction.AlertingNotificationsRead,
|
||||||
|
external: AccessControlAction.AlertingNotificationsExternalRead,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
grafana: AccessControlAction.AlertingNotificationsCreate,
|
||||||
|
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
grafana: AccessControlAction.AlertingNotificationsUpdate,
|
||||||
|
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
grafana: AccessControlAction.AlertingNotificationsDelete,
|
||||||
|
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
read: permissions.read[amVersion],
|
||||||
|
create: permissions.create[amVersion],
|
||||||
|
update: permissions.update[amVersion],
|
||||||
|
delete: permissions.delete[amVersion],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateAccess(actions: AccessControlAction[], fallBackUserRoles: string[]) {
|
||||||
|
return () => {
|
||||||
|
return contextSrv.evaluatePermission(() => fallBackUserRoles, actions);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user