mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: enforce roles on frontend (#33997)
This commit is contained in:
parent
a26507e9c4
commit
8a0dbd0127
@ -101,8 +101,8 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
r.Get("/playlists/", reqSignedIn, hs.Index)
|
r.Get("/playlists/", reqSignedIn, hs.Index)
|
||||||
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
||||||
r.Get("/alerting/", reqEditorRole, hs.Index)
|
r.Get("/alerting/", reqSignedIn, hs.Index)
|
||||||
r.Get("/alerting/*", reqEditorRole, hs.Index)
|
r.Get("/alerting/*", reqSignedIn, hs.Index)
|
||||||
|
|
||||||
// sign up
|
// sign up
|
||||||
r.Get("/verify", hs.Index)
|
r.Get("/verify", hs.Index)
|
||||||
|
@ -214,6 +214,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
||||||
Icon: "comment-alt-share",
|
Icon: "comment-alt-share",
|
||||||
})
|
})
|
||||||
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
||||||
} else {
|
} else {
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
|
||||||
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
||||||
@ -222,10 +223,6 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsNgAlertEnabled() {
|
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
|
||||||
}
|
|
||||||
|
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Alerting",
|
Text: "Alerting",
|
||||||
SubTitle: "Alert rules and notifications",
|
SubTitle: "Alert rules and notifications",
|
||||||
|
@ -43,7 +43,9 @@ export const PanelAlertTabContent: FC<Props> = ({ dashboard, panel }) => {
|
|||||||
<div className={styles.innerWrapper}>
|
<div className={styles.innerWrapper}>
|
||||||
{alert}
|
{alert}
|
||||||
<RulesTable rules={rules} />
|
<RulesTable rules={rules} />
|
||||||
|
{!!dashboard.meta.canSave && (
|
||||||
<NewRuleFromPanelButton className={styles.newButton} panel={panel} dashboard={dashboard} />
|
<NewRuleFromPanelButton className={styles.newButton} panel={panel} dashboard={dashboard} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
@ -52,12 +54,13 @@ export const PanelAlertTabContent: FC<Props> = ({ dashboard, panel }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.noRulesWrapper}>
|
<div className={styles.noRulesWrapper}>
|
||||||
{alert}
|
{alert}
|
||||||
{!!dashboard.uid ? (
|
{!!dashboard.uid && (
|
||||||
<>
|
<>
|
||||||
<p>There are no alert rules linked to this panel.</p>
|
<p>There are no alert rules linked to this panel.</p>
|
||||||
<NewRuleFromPanelButton panel={panel} dashboard={dashboard} />
|
{!!dashboard.meta.canSave && <NewRuleFromPanelButton panel={panel} dashboard={dashboard} />}
|
||||||
</>
|
</>
|
||||||
) : (
|
)}
|
||||||
|
{!dashboard.uid && !!dashboard.meta.canSave && (
|
||||||
<Alert severity="info" title="Dashboard not saved">
|
<Alert severity="info" title="Dashboard not saved">
|
||||||
Dashboard must be saved before alerts can be added.
|
Dashboard must be saved before alerts can be added.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -16,6 +16,7 @@ import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
|
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
jest.mock('./api/alertmanager');
|
jest.mock('./api/alertmanager');
|
||||||
jest.mock('./api/grafana');
|
jest.mock('./api/grafana');
|
||||||
@ -95,6 +96,7 @@ 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;
|
||||||
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { Alert, Button, LoadingPlaceholder } from '@grafana/ui';
|
import { css } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Alert, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
||||||
|
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||||
import { fetchExistingRuleAction } from './state/actions';
|
import { fetchExistingRuleAction } from './state/actions';
|
||||||
import { parseRuleIdentifier } from './utils/rules';
|
import { parseRuleIdentifier } from './utils/rules';
|
||||||
@ -18,13 +22,15 @@ const ExistingRuleEditor: FC<ExistingRuleEditorProps> = ({ identifier }) => {
|
|||||||
useCleanup((state) => state.unifiedAlerting.ruleForm.existingRule);
|
useCleanup((state) => state.unifiedAlerting.ruleForm.existingRule);
|
||||||
const { loading, result, error, dispatched } = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
|
const { loading, result, error, dispatched } = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { isEditable, loading: loadingEditableStatus } = useIsRuleEditable(result?.rule);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dispatched) {
|
if (!dispatched) {
|
||||||
dispatch(fetchExistingRuleAction(identifier));
|
dispatch(fetchExistingRuleAction(identifier));
|
||||||
}
|
}
|
||||||
}, [dispatched, dispatch, identifier]);
|
}, [dispatched, dispatch, identifier]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading || loadingEditableStatus) {
|
||||||
return (
|
return (
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<LoadingPlaceholder text="Loading rule..." />
|
<LoadingPlaceholder text="Loading rule..." />
|
||||||
@ -41,16 +47,10 @@ const ExistingRuleEditor: FC<ExistingRuleEditorProps> = ({ identifier }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return (
|
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
||||||
<Page.Contents>
|
}
|
||||||
<Alert severity="warning" title="Rule not found">
|
if (isEditable === false) {
|
||||||
<p>Sorry! This rule does not exist.</p>
|
return <AlertWarning title="Cannot edit rule">Sorry! You do not have permission to edit this rule.</AlertWarning>;
|
||||||
<a href="/alerting/list">
|
|
||||||
<Button>To rule list</Button>
|
|
||||||
</a>
|
|
||||||
</Alert>
|
|
||||||
</Page.Contents>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <AlertRuleForm existing={result} />;
|
return <AlertRuleForm existing={result} />;
|
||||||
};
|
};
|
||||||
@ -63,7 +63,23 @@ const RuleEditor: FC<RuleEditorProps> = ({ match }) => {
|
|||||||
const identifier = parseRuleIdentifier(decodeURIComponent(id));
|
const identifier = parseRuleIdentifier(decodeURIComponent(id));
|
||||||
return <ExistingRuleEditor key={id} identifier={identifier} />;
|
return <ExistingRuleEditor key={id} identifier={identifier} />;
|
||||||
}
|
}
|
||||||
|
if (!(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor)) {
|
||||||
|
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
|
||||||
|
}
|
||||||
return <AlertRuleForm />;
|
return <AlertRuleForm />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AlertWarning: FC<{ title: string }> = ({ title, children }) => (
|
||||||
|
<Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}>
|
||||||
|
<p>{children}</p>
|
||||||
|
<LinkButton href="alerting/list">To rule list</LinkButton>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const warningStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
warning: css`
|
||||||
|
margin: ${theme.spacing(4)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
export default RuleEditor;
|
export default RuleEditor;
|
||||||
|
@ -18,6 +18,7 @@ import { RuleListGroupView } from './components/rules/RuleListGroupView';
|
|||||||
import { RuleListStateView } from './components/rules/RuleListStateView';
|
import { RuleListStateView } from './components/rules/RuleListStateView';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
groups: RuleListGroupView,
|
groups: RuleListGroupView,
|
||||||
@ -128,12 +129,14 @@ export const RuleList: FC = () => {
|
|||||||
</a>
|
</a>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<div />
|
<div />
|
||||||
|
{(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
|
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
|
||||||
icon="plus"
|
icon="plus"
|
||||||
>
|
>
|
||||||
New alert rule
|
New alert rule
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -16,7 +16,7 @@ import { saveRuleFormAction } from '../../state/actions';
|
|||||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { rulerRuleToFormValues, defaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
import { rulerRuleToFormValues, getDefaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
return rulerRuleToFormValues(existing);
|
return rulerRuleToFormValues(existing);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...defaultFormValues,
|
...getDefaultFormValues(),
|
||||||
queries: getDefaultQueries(),
|
queries: getDefaultQueries(),
|
||||||
...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}),
|
...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}),
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ import { DataSourcePicker } from '@grafana/runtime';
|
|||||||
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
||||||
import { RuleFolderPicker } from './RuleFolderPicker';
|
import { RuleFolderPicker } from './RuleFolderPicker';
|
||||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
const alertTypeOptions: SelectableValue[] = [
|
const alertTypeOptions: SelectableValue[] = [
|
||||||
{
|
{
|
||||||
@ -17,12 +18,15 @@ const alertTypeOptions: SelectableValue[] = [
|
|||||||
value: RuleFormType.threshold,
|
value: RuleFormType.threshold,
|
||||||
description: 'Metric alert based on a defined threshold',
|
description: 'Metric alert based on a defined threshold',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (contextSrv.isEditor) {
|
||||||
|
alertTypeOptions.push({
|
||||||
label: 'System or application',
|
label: 'System or application',
|
||||||
value: RuleFormType.system,
|
value: RuleFormType.system,
|
||||||
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editingExistingRule: boolean;
|
editingExistingRule: boolean;
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import { CallToActionCard } from '@grafana/ui';
|
||||||
|
|
||||||
export const NoRulesSplash: FC = () => (
|
export const NoRulesSplash: FC = () => {
|
||||||
|
if (contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) {
|
||||||
|
return (
|
||||||
<EmptyListCTA
|
<EmptyListCTA
|
||||||
title="You haven`t created any alert rules yet"
|
title="You haven`t created any alert rules yet"
|
||||||
buttonIcon="bell"
|
buttonIcon="bell"
|
||||||
@ -12,4 +16,7 @@ export const NoRulesSplash: FC = () => (
|
|||||||
proTipLinkTitle="Learn more"
|
proTipLinkTitle="Learn more"
|
||||||
proTipTarget="_blank"
|
proTipTarget="_blank"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return <CallToActionCard message="No rules exist yet." callToActionElement={<div />} />;
|
||||||
|
};
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||||
import { Button, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
import { Button, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||||
import { deleteRuleAction } from '../../state/actions';
|
import { deleteRuleAction } from '../../state/actions';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource';
|
import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource';
|
||||||
@ -26,6 +28,8 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
const leftButtons: JSX.Element[] = [];
|
const leftButtons: JSX.Element[] = [];
|
||||||
const rightButtons: JSX.Element[] = [];
|
const rightButtons: JSX.Element[] = [];
|
||||||
|
|
||||||
|
const { isEditable } = useIsRuleEditable(rulerRule);
|
||||||
|
|
||||||
const deleteRule = () => {
|
const deleteRule = () => {
|
||||||
if (ruleToDelete && ruleToDelete.rulerRule) {
|
if (ruleToDelete && ruleToDelete.rulerRule) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -43,7 +47,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// explore does not support grafana rule queries atm
|
// explore does not support grafana rule queries atm
|
||||||
if (isCloudRulesSource(rulesSource)) {
|
if (isCloudRulesSource(rulesSource) && contextSrv.isEditor) {
|
||||||
leftButtons.push(
|
leftButtons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
className={style.button}
|
||||||
@ -108,8 +112,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO check roles
|
if (isEditable && rulerRule) {
|
||||||
if (!!rulerRule) {
|
|
||||||
const editURL = urlUtil.renderUrl(
|
const editURL = urlUtil.renderUrl(
|
||||||
`/alerting/${encodeURIComponent(
|
`/alerting/${encodeURIComponent(
|
||||||
stringifyRuleIdentifier(
|
stringifyRuleIdentifier(
|
||||||
|
@ -13,6 +13,7 @@ import { ActionIcon } from './ActionIcon';
|
|||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import { useFolder } from '../../hooks/useFolder';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
namespace: CombinedRuleNamespace;
|
namespace: CombinedRuleNamespace;
|
||||||
@ -26,6 +27,9 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace }) => {
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
const hasRuler = useHasRuler();
|
const hasRuler = useHasRuler();
|
||||||
|
const rulerRule = group.rules[0]?.rulerRule;
|
||||||
|
const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined;
|
||||||
|
const { folder } = useFolder(folderUID);
|
||||||
|
|
||||||
const stats = useMemo(
|
const stats = useMemo(
|
||||||
(): Record<PromAlertingRuleState, number> =>
|
(): Record<PromAlertingRuleState, number> =>
|
||||||
@ -65,11 +69,14 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace }) => {
|
|||||||
|
|
||||||
// for grafana, link to folder views
|
// for grafana, link to folder views
|
||||||
if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
|
if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
|
||||||
const rulerRule = group.rules[0]?.rulerRule;
|
|
||||||
const folderUID = rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid;
|
|
||||||
if (folderUID) {
|
if (folderUID) {
|
||||||
const baseUrl = `/dashboards/f/${folderUID}/${kbn.slugifyForUrl(namespace.name)}`;
|
const baseUrl = `/dashboards/f/${folderUID}/${kbn.slugifyForUrl(namespace.name)}`;
|
||||||
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" to={baseUrl + '/settings'} target="__blank" />);
|
if (folder?.canSave) {
|
||||||
|
actionIcons.push(
|
||||||
|
<ActionIcon key="edit" icon="pen" tooltip="edit" to={baseUrl + '/settings'} target="__blank" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (folder?.canAdmin) {
|
||||||
actionIcons.push(
|
actionIcons.push(
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
key="manage-perms"
|
key="manage-perms"
|
||||||
@ -79,6 +86,7 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace }) => {
|
|||||||
target="__blank"
|
target="__blank"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else if (hasRuler(rulesSource)) {
|
} else if (hasRuler(rulesSource)) {
|
||||||
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" />); // @TODO
|
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" />); // @TODO
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { CallToActionCard } from '@grafana/ui';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
|
|
||||||
@ -6,11 +8,16 @@ type Props = {
|
|||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoSilencesSplash: FC<Props> = ({ alertManagerSourceName }) => (
|
export const NoSilencesSplash: FC<Props> = ({ alertManagerSourceName }) => {
|
||||||
|
if (contextSrv.isEditor) {
|
||||||
|
return (
|
||||||
<EmptyListCTA
|
<EmptyListCTA
|
||||||
title="You haven't created any silences yet"
|
title="You haven't created any silences yet"
|
||||||
buttonIcon="bell-slash"
|
buttonIcon="bell-slash"
|
||||||
buttonLink={makeAMLink('alerting/silence/new', alertManagerSourceName)}
|
buttonLink={makeAMLink('alerting/silence/new', alertManagerSourceName)}
|
||||||
buttonTitle="New silence"
|
buttonTitle="New silence"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return <CallToActionCard callToActionElement={<div />} message="No silences found." />;
|
||||||
|
};
|
||||||
|
@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { Matchers } from './Matchers';
|
import { Matchers } from './Matchers';
|
||||||
import { SilenceStateTag } from './SilenceStateTag';
|
import { SilenceStateTag } from './SilenceStateTag';
|
||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
silence: Silence;
|
silence: Silence;
|
||||||
@ -35,6 +36,8 @@ const SilenceTableRow: FC<Props> = ({ silence, className, silencedAlerts, alertM
|
|||||||
dispatch(expireSilenceAction(alertManagerSourceName, silence.id));
|
dispatch(expireSilenceAction(alertManagerSourceName, silence.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detailsColspan = contextSrv.isEditor ? 4 : 3;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
@ -53,6 +56,7 @@ const SilenceTableRow: FC<Props> = ({ silence, className, silencedAlerts, alertM
|
|||||||
<br />
|
<br />
|
||||||
{endsAtDate?.format(dateDisplayFormat)}
|
{endsAtDate?.format(dateDisplayFormat)}
|
||||||
</td>
|
</td>
|
||||||
|
{contextSrv.isEditor && (
|
||||||
<td className={styles.actionsCell}>
|
<td className={styles.actionsCell}>
|
||||||
{status.state === 'expired' ? (
|
{status.state === 'expired' ? (
|
||||||
<Link href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}>
|
<Link href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}>
|
||||||
@ -71,36 +75,37 @@ const SilenceTableRow: FC<Props> = ({ silence, className, silencedAlerts, alertM
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<>
|
<>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td />
|
<td />
|
||||||
<td>Comment</td>
|
<td>Comment</td>
|
||||||
<td colSpan={4}>{comment}</td>
|
<td colSpan={detailsColspan}>{comment}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td />
|
<td />
|
||||||
<td>Schedule</td>
|
<td>Schedule</td>
|
||||||
<td colSpan={4}>{`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(
|
<td colSpan={detailsColspan}>{`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(
|
||||||
dateDisplayFormat
|
dateDisplayFormat
|
||||||
)}`}</td>
|
)}`}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td />
|
<td />
|
||||||
<td>Duration</td>
|
<td>Duration</td>
|
||||||
<td colSpan={4}>{duration}</td>
|
<td colSpan={detailsColspan}>{duration}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td />
|
<td />
|
||||||
<td>Created by</td>
|
<td>Created by</td>
|
||||||
<td colSpan={4}>{createdBy}</td>
|
<td colSpan={detailsColspan}>{createdBy}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{!!silencedAlerts.length && (
|
{!!silencedAlerts.length && (
|
||||||
<tr className={cx(className, styles.alertRulesCell)}>
|
<tr className={cx(className, styles.alertRulesCell)}>
|
||||||
<td />
|
<td />
|
||||||
<td>Affected alerts</td>
|
<td>Affected alerts</td>
|
||||||
<td colSpan={4}>
|
<td colSpan={detailsColspan}>
|
||||||
<SilencedAlertsTable silencedAlerts={silencedAlerts} />
|
<SilencedAlertsTable silencedAlerts={silencedAlerts} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -7,6 +7,7 @@ import SilenceTableRow from './SilenceTableRow';
|
|||||||
import { getAlertTableStyles } from '../../styles/table';
|
import { getAlertTableStyles } from '../../styles/table';
|
||||||
import { NoSilencesSplash } from './NoSilencesCTA';
|
import { NoSilencesSplash } from './NoSilencesCTA';
|
||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
interface Props {
|
interface Props {
|
||||||
silences: Silence[];
|
silences: Silence[];
|
||||||
alertManagerAlerts: AlertmanagerAlert[];
|
alertManagerAlerts: AlertmanagerAlert[];
|
||||||
@ -25,6 +26,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
<>
|
<>
|
||||||
{!!silences.length && (
|
{!!silences.length && (
|
||||||
<>
|
<>
|
||||||
|
{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">
|
||||||
@ -32,6 +34,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className={tableStyles.colExpand} />
|
<col className={tableStyles.colExpand} />
|
||||||
@ -39,7 +42,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
<col className={styles.colMatchers} />
|
<col className={styles.colMatchers} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
{contextSrv.isEditor && <col />}
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -48,7 +51,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
<th>Matchers</th>
|
<th>Matchers</th>
|
||||||
<th>Alerts</th>
|
<th>Alerts</th>
|
||||||
<th>Schedule</th>
|
<th>Schedule</th>
|
||||||
<th>Action</th>
|
{contextSrv.isEditor && <th>Action</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
32
public/app/features/alerting/unified/hooks/useFolder.ts
Normal file
32
public/app/features/alerting/unified/hooks/useFolder.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { FolderDTO } from 'app/types';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { fetchFolderIfNotFetchedAction } from '../state/actions';
|
||||||
|
import { initialAsyncRequestState } from '../utils/redux';
|
||||||
|
|
||||||
|
interface ReturnBag {
|
||||||
|
folder?: FolderDTO;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFolder(uid?: string): ReturnBag {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const folderRequests = useUnifiedAlertingSelector((state) => state.folders);
|
||||||
|
useEffect(() => {
|
||||||
|
if (uid) {
|
||||||
|
dispatch(fetchFolderIfNotFetchedAction(uid));
|
||||||
|
}
|
||||||
|
}, [dispatch, uid]);
|
||||||
|
|
||||||
|
if (uid) {
|
||||||
|
const request = folderRequests[uid] || initialAsyncRequestState;
|
||||||
|
return {
|
||||||
|
folder: request.result,
|
||||||
|
loading: request.loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { isGrafanaRulerRule } from '../utils/rules';
|
||||||
|
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
import { useFolder } from './useFolder';
|
||||||
|
|
||||||
|
interface ResultBag {
|
||||||
|
isEditable?: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsRuleEditable(rule?: RulerRuleDTO): ResultBag {
|
||||||
|
const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined;
|
||||||
|
|
||||||
|
const { folder, loading } = useFolder(folderUID);
|
||||||
|
|
||||||
|
if (!rule) {
|
||||||
|
return { isEditable: false, loading: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// grafana rules can be edited if user can edit the folder they're in
|
||||||
|
if (isGrafanaRulerRule(rule)) {
|
||||||
|
if (!folderUID) {
|
||||||
|
throw new Error(
|
||||||
|
`Rule ${rule.grafana_alert.title} does not have a folder uid, cannot determine if it is editable.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isEditable: folder?.canSave,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// prom rules are only editable by users with Editor role
|
||||||
|
return {
|
||||||
|
isEditable: contextSrv.isEditor,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
@ -8,7 +8,7 @@ import {
|
|||||||
Silence,
|
Silence,
|
||||||
SilenceCreatePayload,
|
SilenceCreatePayload,
|
||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { NotifierDTO, ThunkResult } from 'app/types';
|
import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types';
|
||||||
import { RuleIdentifier, RuleNamespace, RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleIdentifier, RuleNamespace, RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import {
|
import {
|
||||||
PostableRulerRuleGroupDTO,
|
PostableRulerRuleGroupDTO,
|
||||||
@ -48,6 +48,7 @@ import {
|
|||||||
stringifyRuleIdentifier,
|
stringifyRuleIdentifier,
|
||||||
} from '../utils/rules';
|
} from '../utils/rules';
|
||||||
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager-config';
|
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager-config';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
export const fetchPromRulesAction = createAsyncThunk(
|
export const fetchPromRulesAction = createAsyncThunk(
|
||||||
'unifiedalerting/fetchPromRules',
|
'unifiedalerting/fetchPromRules',
|
||||||
@ -460,3 +461,16 @@ export const deleteTemplateAction = (templateName: string, alertManagerSourceNam
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchFolderAction = createAsyncThunk(
|
||||||
|
'unifiedalerting/fetchFolder',
|
||||||
|
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!getState().unifiedAlerting.folders[uid]?.dispatched) {
|
||||||
|
dispatch(fetchFolderAction(uid));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
saveRuleFormAction,
|
saveRuleFormAction,
|
||||||
updateAlertManagerConfigAction,
|
updateAlertManagerConfigAction,
|
||||||
createOrUpdateSilenceAction,
|
createOrUpdateSilenceAction,
|
||||||
|
fetchFolderAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
export const reducer = combineReducers({
|
export const reducer = combineReducers({
|
||||||
@ -32,6 +33,7 @@ export const reducer = combineReducers({
|
|||||||
updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
|
updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,
|
||||||
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
|
amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName)
|
||||||
.reducer,
|
.reducer,
|
||||||
|
folders: createAsyncMapSlice('folders', fetchFolderAction, (uid) => uid).reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DataQuery, getDefaultTimeRange, rangeUtil, RelativeTimeRange } from '@grafana/data';
|
import { DataQuery, getDefaultTimeRange, rangeUtil, RelativeTimeRange } from '@grafana/data';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||||
@ -22,11 +23,13 @@ import { arrayToRecord, recordToArray } from './misc';
|
|||||||
import { isAlertingRulerRule, isGrafanaRulerRule } from './rules';
|
import { isAlertingRulerRule, isGrafanaRulerRule } from './rules';
|
||||||
import { parseInterval } from './time';
|
import { parseInterval } from './time';
|
||||||
|
|
||||||
export const defaultFormValues: RuleFormValues = Object.freeze({
|
export const getDefaultFormValues = (): RuleFormValues =>
|
||||||
|
Object.freeze({
|
||||||
name: '',
|
name: '',
|
||||||
labels: [{ key: '', value: '' }],
|
labels: [{ key: '', value: '' }],
|
||||||
annotations: [{ key: '', value: '' }],
|
annotations: [{ key: '', value: '' }],
|
||||||
dataSourceName: null,
|
dataSourceName: null,
|
||||||
|
type: !contextSrv.isEditor ? RuleFormType.threshold : undefined, // viewers can't create prom alerts
|
||||||
|
|
||||||
// threshold
|
// threshold
|
||||||
folder: null,
|
folder: null,
|
||||||
@ -43,7 +46,7 @@ export const defaultFormValues: RuleFormValues = Object.freeze({
|
|||||||
expression: '',
|
expression: '',
|
||||||
forTime: 1,
|
forTime: 1,
|
||||||
forTimeUnit: 'm',
|
forTimeUnit: 'm',
|
||||||
});
|
});
|
||||||
|
|
||||||
export function formValuesToRulerAlertingRuleDTO(values: RuleFormValues): RulerAlertingRuleDTO {
|
export function formValuesToRulerAlertingRuleDTO(values: RuleFormValues): RulerAlertingRuleDTO {
|
||||||
const { name, expression, forTime, forTimeUnit } = values;
|
const { name, expression, forTime, forTimeUnit } = values;
|
||||||
@ -81,6 +84,8 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
|
|||||||
|
|
||||||
export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues {
|
export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues {
|
||||||
const { ruleSourceName, namespace, group, rule } = ruleWithLocation;
|
const { ruleSourceName, namespace, group, rule } = ruleWithLocation;
|
||||||
|
|
||||||
|
const defaultFormValues = getDefaultFormValues();
|
||||||
if (isGrafanaRulesSource(ruleSourceName)) {
|
if (isGrafanaRulesSource(ruleSourceName)) {
|
||||||
if (isGrafanaRulerRule(rule)) {
|
if (isGrafanaRulerRule(rule)) {
|
||||||
const ga = rule.grafana_alert;
|
const ga = rule.grafana_alert;
|
||||||
|
@ -22,15 +22,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"reqRole": "Admin"
|
"reqRole": "Editor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"reqRole": "Admin"
|
"reqRole": "Editor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
"reqRole": "Admin"
|
"reqRole": "Editor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
@ -350,7 +350,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/routes',
|
path: '/alerting/routes',
|
||||||
roles: () => ['Admin'],
|
roles: () => ['Admin', 'Editor'],
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
||||||
),
|
),
|
||||||
@ -363,42 +363,49 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silence/new',
|
path: '/alerting/silence/new',
|
||||||
|
roles: () => ['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: () => ['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.featureToggles.ngalert ? () => ['Editor', 'Admin'] : undefined,
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/templates/new',
|
path: '/alerting/notifications/templates/new',
|
||||||
|
roles: () => ['Editor', 'Admin'],
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/templates/:id/edit',
|
path: '/alerting/notifications/templates/:id/edit',
|
||||||
|
roles: () => ['Editor', 'Admin'],
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/receivers/new',
|
path: '/alerting/notifications/receivers/new',
|
||||||
|
roles: () => ['Editor', 'Admin'],
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications/receivers/:id/edit',
|
path: '/alerting/notifications/receivers/:id/edit',
|
||||||
|
roles: () => ['Editor', 'Admin'],
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user