diff --git a/pkg/api/api.go b/pkg/api/api.go index 17858fdb855..36caad40923 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -101,8 +101,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) - r.Get("/alerting/", reqEditorRole, hs.Index) - r.Get("/alerting/*", reqEditorRole, hs.Index) + r.Get("/alerting/", reqSignedIn, hs.Index) + r.Get("/alerting/*", reqSignedIn, hs.Index) // sign up r.Get("/verify", hs.Index) diff --git a/pkg/api/index.go b/pkg/api/index.go index f98366d27ee..33515f0e585 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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", Icon: "comment-alt-share", }) + alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"}) } else { alertChildNavs = append(alertChildNavs, &dtos.NavLink{ 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{ Text: "Alerting", SubTitle: "Alert rules and notifications", diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.tsx index 1df8ad0689a..8cef6ef04d5 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.tsx @@ -43,7 +43,9 @@ export const PanelAlertTabContent: FC = ({ dashboard, panel }) => {
{alert} - + {!!dashboard.meta.canSave && ( + + )}
); @@ -52,12 +54,13 @@ export const PanelAlertTabContent: FC = ({ dashboard, panel }) => { return (
{alert} - {!!dashboard.uid ? ( + {!!dashboard.uid && ( <>

There are no alert rules linked to this panel.

- + {!!dashboard.meta.canSave && } - ) : ( + )} + {!dashboard.uid && !!dashboard.meta.canSave && ( Dashboard must be saved before alerts can be added. diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index 76b8788c926..a29dead527d 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -16,6 +16,7 @@ import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector' import userEvent from '@testing-library/user-event'; import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; import store from 'app/core/store'; +import { contextSrv } from 'app/core/services/context_srv'; jest.mock('./api/alertmanager'); jest.mock('./api/grafana'); @@ -95,6 +96,7 @@ describe('Receivers', () => { mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); setDataSourceSrv(new MockDataSourceSrv(dataSources)); + contextSrv.isEditor = true; store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); }); diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 8db86fc4219..94de68aaf72 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -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 { contextSrv } from 'app/core/services/context_srv'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { RuleIdentifier } from 'app/types/unified-alerting'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { AlertRuleForm } from './components/rule-editor/AlertRuleForm'; +import { useIsRuleEditable } from './hooks/useIsRuleEditable'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchExistingRuleAction } from './state/actions'; import { parseRuleIdentifier } from './utils/rules'; @@ -18,13 +22,15 @@ const ExistingRuleEditor: FC = ({ identifier }) => { useCleanup((state) => state.unifiedAlerting.ruleForm.existingRule); const { loading, result, error, dispatched } = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule); const dispatch = useDispatch(); + const { isEditable, loading: loadingEditableStatus } = useIsRuleEditable(result?.rule); + useEffect(() => { if (!dispatched) { dispatch(fetchExistingRuleAction(identifier)); } }, [dispatched, dispatch, identifier]); - if (loading) { + if (loading || loadingEditableStatus) { return ( @@ -41,16 +47,10 @@ const ExistingRuleEditor: FC = ({ identifier }) => { ); } if (!result) { - return ( - - -

Sorry! This rule does not exist.

- - - -
-
- ); + return Sorry! This rule does not exist.; + } + if (isEditable === false) { + return Sorry! You do not have permission to edit this rule.; } return ; }; @@ -63,7 +63,23 @@ const RuleEditor: FC = ({ match }) => { const identifier = parseRuleIdentifier(decodeURIComponent(id)); return ; } + if (!(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor)) { + return Sorry! You are not allowed to create rules.; + } return ; }; +const AlertWarning: FC<{ title: string }> = ({ title, children }) => ( + +

{children}

+ To rule list +
+); + +const warningStyles = (theme: GrafanaTheme2) => ({ + warning: css` + margin: ${theme.spacing(4)}; + `, +}); + export default RuleEditor; diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 3de562a51ea..db6f03f503c 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -18,6 +18,7 @@ import { RuleListGroupView } from './components/rules/RuleListGroupView'; import { RuleListStateView } from './components/rules/RuleListStateView'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useLocation } from 'react-router-dom'; +import { contextSrv } from 'app/core/services/context_srv'; const VIEWS = { groups: RuleListGroupView, @@ -128,12 +129,14 @@ export const RuleList: FC = () => {
- - New alert rule - + {(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && ( + + New alert rule + + )}
)} diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index f413969cfea..735102cd65a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -16,7 +16,7 @@ import { saveRuleFormAction } from '../../state/actions'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { useDispatch } from 'react-redux'; 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 { useQueryParams } from 'app/core/hooks/useQueryParams'; @@ -36,7 +36,7 @@ export const AlertRuleForm: FC = ({ existing }) => { return rulerRuleToFormValues(existing); } return { - ...defaultFormValues, + ...getDefaultFormValues(), queries: getDefaultQueries(), ...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}), }; diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx index e0ff877d211..08bdbd270a2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx @@ -10,6 +10,7 @@ import { DataSourcePicker } from '@grafana/runtime'; import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler'; import { RuleFolderPicker } from './RuleFolderPicker'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; +import { contextSrv } from 'app/core/services/context_srv'; const alertTypeOptions: SelectableValue[] = [ { @@ -17,12 +18,15 @@ const alertTypeOptions: SelectableValue[] = [ value: RuleFormType.threshold, description: 'Metric alert based on a defined threshold', }, - { +]; + +if (contextSrv.isEditor) { + alertTypeOptions.push({ label: 'System or application', value: RuleFormType.system, description: 'Alert based on a system or application behavior. Based on Prometheus.', - }, -]; + }); +} interface Props { editingExistingRule: boolean; diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index 4cf72a9304c..2a74d453395 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -1,15 +1,22 @@ import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { contextSrv } from 'app/core/services/context_srv'; import React, { FC } from 'react'; +import { CallToActionCard } from '@grafana/ui'; -export const NoRulesSplash: FC = () => ( - -); +export const NoRulesSplash: FC = () => { + if (contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) { + return ( + + ); + } + return } />; +}; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 97ce5f25d7b..99bb0021880 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -1,10 +1,12 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, urlUtil } from '@grafana/data'; 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 React, { FC, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { useIsRuleEditable } from '../../hooks/useIsRuleEditable'; import { deleteRuleAction } from '../../state/actions'; import { Annotation } from '../../utils/constants'; import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource'; @@ -26,6 +28,8 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { const leftButtons: JSX.Element[] = []; const rightButtons: JSX.Element[] = []; + const { isEditable } = useIsRuleEditable(rulerRule); + const deleteRule = () => { if (ruleToDelete && ruleToDelete.rulerRule) { dispatch( @@ -43,7 +47,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { }; // explore does not support grafana rule queries atm - if (isCloudRulesSource(rulesSource)) { + if (isCloudRulesSource(rulesSource) && contextSrv.isEditor) { leftButtons.push( = ({ rule, rulesSource }) => { } } - // @TODO check roles - if (!!rulerRule) { + if (isEditable && rulerRule) { const editURL = urlUtil.renderUrl( `/alerting/${encodeURIComponent( stringifyRuleIdentifier( diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index e4ab3dc9789..09d4746b7df 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -13,6 +13,7 @@ import { ActionIcon } from './ActionIcon'; import pluralize from 'pluralize'; import { useHasRuler } from '../../hooks/useHasRuler'; import kbn from 'app/core/utils/kbn'; +import { useFolder } from '../../hooks/useFolder'; interface Props { namespace: CombinedRuleNamespace; @@ -26,6 +27,9 @@ export const RulesGroup: FC = React.memo(({ group, namespace }) => { const [isCollapsed, setIsCollapsed] = useState(true); 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( (): Record => @@ -65,20 +69,24 @@ export const RulesGroup: FC = React.memo(({ group, namespace }) => { // for grafana, link to folder views if (rulesSource === GRAFANA_RULES_SOURCE_NAME) { - const rulerRule = group.rules[0]?.rulerRule; - const folderUID = rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid; if (folderUID) { const baseUrl = `/dashboards/f/${folderUID}/${kbn.slugifyForUrl(namespace.name)}`; - actionIcons.push(); - actionIcons.push( - - ); + if (folder?.canSave) { + actionIcons.push( + + ); + } + if (folder?.canAdmin) { + actionIcons.push( + + ); + } } else if (hasRuler(rulesSource)) { actionIcons.push(); // @TODO } diff --git a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx index f37aee1ea26..95a298338c1 100644 --- a/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx +++ b/public/app/features/alerting/unified/components/silences/NoSilencesCTA.tsx @@ -1,4 +1,6 @@ +import { CallToActionCard } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { contextSrv } from 'app/core/services/context_srv'; import React, { FC } from 'react'; import { makeAMLink } from '../../utils/misc'; @@ -6,11 +8,16 @@ type Props = { alertManagerSourceName: string; }; -export const NoSilencesSplash: FC = ({ alertManagerSourceName }) => ( - -); +export const NoSilencesSplash: FC = ({ alertManagerSourceName }) => { + if (contextSrv.isEditor) { + return ( + + ); + } + return } message="No silences found." />; +}; diff --git a/public/app/features/alerting/unified/components/silences/SilenceTableRow.tsx b/public/app/features/alerting/unified/components/silences/SilenceTableRow.tsx index efab9b5e3a6..488035fb1f5 100644 --- a/public/app/features/alerting/unified/components/silences/SilenceTableRow.tsx +++ b/public/app/features/alerting/unified/components/silences/SilenceTableRow.tsx @@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux'; import { Matchers } from './Matchers'; import { SilenceStateTag } from './SilenceStateTag'; import { makeAMLink } from '../../utils/misc'; +import { contextSrv } from 'app/core/services/context_srv'; interface Props { className?: string; silence: Silence; @@ -35,6 +36,8 @@ const SilenceTableRow: FC = ({ silence, className, silencedAlerts, alertM dispatch(expireSilenceAction(alertManagerSourceName, silence.id)); }; + const detailsColspan = contextSrv.isEditor ? 4 : 3; + return ( @@ -53,54 +56,56 @@ const SilenceTableRow: FC = ({ silence, className, silencedAlerts, alertM
{endsAtDate?.format(dateDisplayFormat)} - - {status.state === 'expired' ? ( - - Recreate - - ) : ( - - Unsilence - - )} - {status.state !== 'expired' && ( - - )} - + {contextSrv.isEditor && ( + + {status.state === 'expired' ? ( + + Recreate + + ) : ( + + Unsilence + + )} + {status.state !== 'expired' && ( + + )} + + )} {!isCollapsed && ( <> Comment - {comment} + {comment} Schedule - {`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format( + {`${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format( dateDisplayFormat )}`} Duration - {duration} + {duration} Created by - {createdBy} + {createdBy} {!!silencedAlerts.length && ( Affected alerts - + diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index e32d6c20b78..dae086effa1 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -7,6 +7,7 @@ import SilenceTableRow from './SilenceTableRow'; import { getAlertTableStyles } from '../../styles/table'; import { NoSilencesSplash } from './NoSilencesCTA'; import { makeAMLink } from '../../utils/misc'; +import { contextSrv } from 'app/core/services/context_srv'; interface Props { silences: Silence[]; alertManagerAlerts: AlertmanagerAlert[]; @@ -25,13 +26,15 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo <> {!!silences.length && ( <> -
- - - -
+ {contextSrv.isEditor && ( +
+ + + +
+ )} @@ -39,7 +42,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo - + {contextSrv.isEditor && } @@ -48,7 +51,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo - + {contextSrv.isEditor && } diff --git a/public/app/features/alerting/unified/hooks/useFolder.ts b/public/app/features/alerting/unified/hooks/useFolder.ts new file mode 100644 index 00000000000..196c0406456 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useFolder.ts @@ -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, + }; +} diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts new file mode 100644 index 00000000000..3e3c93a338e --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts @@ -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, + }; +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index fdd93d76d41..356eed2cff5 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -8,7 +8,7 @@ import { Silence, SilenceCreatePayload, } 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 { PostableRulerRuleGroupDTO, @@ -48,6 +48,7 @@ import { stringifyRuleIdentifier, } from '../utils/rules'; import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager-config'; +import { backendSrv } from 'app/core/services/backend_srv'; export const fetchPromRulesAction = createAsyncThunk( 'unifiedalerting/fetchPromRules', @@ -460,3 +461,16 @@ export const deleteTemplateAction = (templateName: string, alertManagerSourceNam ); }; }; + +export const fetchFolderAction = createAsyncThunk( + 'unifiedalerting/fetchFolder', + (uid: string): Promise => withSerializedError(backendSrv.getFolderByUid(uid)) +); + +export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult => { + return (dispatch, getState) => { + if (!getState().unifiedAlerting.folders[uid]?.dispatched) { + dispatch(fetchFolderAction(uid)); + } + }; +}; diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts index 73d8554105f..2a68b7c2609 100644 --- a/public/app/features/alerting/unified/state/reducers.ts +++ b/public/app/features/alerting/unified/state/reducers.ts @@ -11,6 +11,7 @@ import { saveRuleFormAction, updateAlertManagerConfigAction, createOrUpdateSilenceAction, + fetchFolderAction, } from './actions'; export const reducer = combineReducers({ @@ -32,6 +33,7 @@ export const reducer = combineReducers({ updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer, amAlerts: createAsyncMapSlice('amAlerts', fetchAmAlertsAction, (alertManagerSourceName) => alertManagerSourceName) .reducer, + folders: createAsyncMapSlice('folders', fetchFolderAction, (uid) => uid).reducer, }); export type UnifiedAlertingState = ReturnType; diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 1780df007cd..d80e409d417 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,5 +1,6 @@ import { DataQuery, getDefaultTimeRange, rangeUtil, RelativeTimeRange } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; import { getNextRefIdChar } from 'app/core/utils/query'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; @@ -22,28 +23,30 @@ import { arrayToRecord, recordToArray } from './misc'; import { isAlertingRulerRule, isGrafanaRulerRule } from './rules'; import { parseInterval } from './time'; -export const defaultFormValues: RuleFormValues = Object.freeze({ - name: '', - labels: [{ key: '', value: '' }], - annotations: [{ key: '', value: '' }], - dataSourceName: null, +export const getDefaultFormValues = (): RuleFormValues => + Object.freeze({ + name: '', + labels: [{ key: '', value: '' }], + annotations: [{ key: '', value: '' }], + dataSourceName: null, + type: !contextSrv.isEditor ? RuleFormType.threshold : undefined, // viewers can't create prom alerts - // threshold - folder: null, - queries: [], - condition: '', - noDataState: GrafanaAlertStateDecision.NoData, - execErrState: GrafanaAlertStateDecision.Alerting, - evaluateEvery: '1m', - evaluateFor: '5m', + // threshold + folder: null, + queries: [], + condition: '', + noDataState: GrafanaAlertStateDecision.NoData, + execErrState: GrafanaAlertStateDecision.Alerting, + evaluateEvery: '1m', + evaluateFor: '5m', - // system - group: '', - namespace: '', - expression: '', - forTime: 1, - forTimeUnit: 'm', -}); + // system + group: '', + namespace: '', + expression: '', + forTime: 1, + forTimeUnit: 'm', + }); export function formValuesToRulerAlertingRuleDTO(values: RuleFormValues): RulerAlertingRuleDTO { const { name, expression, forTime, forTimeUnit } = values; @@ -81,6 +84,8 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { const { ruleSourceName, namespace, group, rule } = ruleWithLocation; + + const defaultFormValues = getDefaultFormValues(); if (isGrafanaRulesSource(ruleSourceName)) { if (isGrafanaRulerRule(rule)) { const ga = rule.grafana_alert; diff --git a/public/app/plugins/datasource/alertmanager/plugin.json b/public/app/plugins/datasource/alertmanager/plugin.json index 088bdd5adef..b39a18847c1 100644 --- a/public/app/plugins/datasource/alertmanager/plugin.json +++ b/public/app/plugins/datasource/alertmanager/plugin.json @@ -22,15 +22,15 @@ }, { "method": "POST", - "reqRole": "Admin" + "reqRole": "Editor" }, { "method": "PUT", - "reqRole": "Admin" + "reqRole": "Editor" }, { "method": "DELETE", - "reqRole": "Admin" + "reqRole": "Editor" }, { "method": "GET", diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 52221c8528c..eca7889324d 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -350,7 +350,7 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/alerting/routes', - roles: () => ['Admin'], + roles: () => ['Admin', 'Editor'], component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes') ), @@ -363,42 +363,49 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/alerting/silence/new', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), }, { path: '/alerting/silence/:id/edit', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), }, { path: '/alerting/notifications', + roles: config.featureToggles.ngalert ? () => ['Editor', 'Admin'] : undefined, component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex') ), }, { path: '/alerting/notifications/templates/new', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex') ), }, { path: '/alerting/notifications/templates/:id/edit', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex') ), }, { path: '/alerting/notifications/receivers/new', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex') ), }, { path: '/alerting/notifications/receivers/:id/edit', + roles: () => ['Editor', 'Admin'], component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex') ),
Matchers Alerts ScheduleActionAction