mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 19:00:54 -06:00
Alerting: receivers page + template list (#33112)
This commit is contained in:
parent
382cab6406
commit
60b469f836
@ -206,15 +206,23 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
if hs.Cfg.IsNgAlertEnabled() {
|
if hs.Cfg.IsNgAlertEnabled() {
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
||||||
}
|
}
|
||||||
|
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
|
||||||
|
if hs.Cfg.IsNgAlertEnabled() {
|
||||||
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
|
||||||
|
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
||||||
|
Icon: "comment-alt-share",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
|
||||||
|
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
||||||
|
Icon: "comment-alt-share",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsNgAlertEnabled() {
|
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"})
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
||||||
}
|
}
|
||||||
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
|
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
|
|
||||||
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
|
|
||||||
Icon: "comment-alt-share",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Alerting",
|
Text: "Alerting",
|
||||||
|
7
public/app/features/alerting/NotificationsIndex.tsx
Normal file
7
public/app/features/alerting/NotificationsIndex.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import NotificationsListPage from './NotificationsListPage';
|
||||||
|
import Receivers from './unified/Receivers';
|
||||||
|
|
||||||
|
// route between unified and "old" alerting pages based on feature flag
|
||||||
|
|
||||||
|
export default config.featureToggles.ngalert ? Receivers : NotificationsListPage;
|
@ -1,4 +1,4 @@
|
|||||||
import { InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
import { Field, InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
@ -22,7 +22,9 @@ const AmRoutes: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper pageId="am-routes">
|
<AlertingPageWrapper pageId="am-routes">
|
||||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
<Field label="Choose alert manager">
|
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
</Field>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
{error && !loading && (
|
{error && !loading && (
|
||||||
|
40
public/app/features/alerting/unified/Receivers.tsx
Normal file
40
public/app/features/alerting/unified/Receivers.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Field, InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
|
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||||
|
import { TemplatesTable } from './components/receivers/TemplatesTable';
|
||||||
|
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||||
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||||
|
import { fetchAlertManagerConfigAction } from './state/actions';
|
||||||
|
import { initialAsyncRequestState } from './utils/redux';
|
||||||
|
|
||||||
|
const Receivers: FC = () => {
|
||||||
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const config = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||||
|
}, [alertManagerSourceName, dispatch]);
|
||||||
|
|
||||||
|
const { result, loading, error } = config[alertManagerSourceName] || initialAsyncRequestState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertingPageWrapper pageId="receivers">
|
||||||
|
<Field label="Choose alert manager">
|
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
</Field>
|
||||||
|
{error && !loading && (
|
||||||
|
<InfoBox severity="error" title={<h4>Error loading alert manager config</h4>}>
|
||||||
|
{error.message || 'Unknown error.'}
|
||||||
|
</InfoBox>
|
||||||
|
)}
|
||||||
|
{loading && <LoadingPlaceholder text="loading receivers..." />}
|
||||||
|
{result && !loading && !error && <TemplatesTable config={result} />}
|
||||||
|
</AlertingPageWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Receivers;
|
@ -1,4 +1,4 @@
|
|||||||
import { InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
import { Field, InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
@ -22,7 +22,9 @@ const Silences: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper pageId="silences">
|
<AlertingPageWrapper pageId="silences">
|
||||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
<Field label="Choose alert manager">
|
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
</Field>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
{error && !loading && (
|
{error && !loading && (
|
||||||
|
@ -19,7 +19,10 @@ export async function fetchAlertManagerConfig(alertmanagerSourceName: string): P
|
|||||||
showSuccessAlert: false,
|
showSuccessAlert: false,
|
||||||
})
|
})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
return result.data;
|
return {
|
||||||
|
template_files: result.data.template_files ?? {},
|
||||||
|
alertmanager_config: result.data.alertmanager_config ?? {},
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if no config has been uploaded to grafana, it returns error instead of latest config
|
// if no config has been uploaded to grafana, it returns error instead of latest config
|
||||||
if (
|
if (
|
||||||
|
@ -31,6 +31,7 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
width={29}
|
||||||
className="ds-picker select-container"
|
className="ds-picker select-container"
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { Button, useStyles } from '@grafana/ui';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
addButtonLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReceiversSection: FC<Props> = ({ title, description, addButtonLabel, children }) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.heading}>
|
||||||
|
<div>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<p className={styles.description}>{description}</p>
|
||||||
|
</div>
|
||||||
|
<Button icon="plus">{addButtonLabel}</Button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
heading: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`,
|
||||||
|
description: css`
|
||||||
|
color: ${theme.colors.textSemiWeak};
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,80 @@
|
|||||||
|
import { useStyles } from '@grafana/ui';
|
||||||
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import React, { FC, Fragment, useMemo, useState } from 'react';
|
||||||
|
import { getAlertTableStyles } from '../../styles/table';
|
||||||
|
import { CollapseToggle } from '../CollapseToggle';
|
||||||
|
import { DetailsField } from '../DetailsField';
|
||||||
|
import { ActionButton } from '../rules/ActionButton';
|
||||||
|
import { ActionIcon } from '../rules/ActionIcon';
|
||||||
|
import { ReceiversSection } from './ReceiversSection';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
config: AlertManagerCortexConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplatesTable: FC<Props> = ({ config }) => {
|
||||||
|
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
|
||||||
|
const tableStyles = useStyles(getAlertTableStyles);
|
||||||
|
|
||||||
|
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReceiversSection
|
||||||
|
title="Message templates"
|
||||||
|
description="Templates construct the messages that get sent to the contact points."
|
||||||
|
addButtonLabel="New templates"
|
||||||
|
>
|
||||||
|
<table className={tableStyles.table}>
|
||||||
|
<colgroup>
|
||||||
|
<col className={tableStyles.colExpand} />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Template</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{!templateRows.length && (
|
||||||
|
<tr className={tableStyles.evenRow}>
|
||||||
|
<td colSpan={3}>No templates defined.</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{templateRows.map(([name, content], idx) => {
|
||||||
|
const isExpanded = !!expandedTemplates[name];
|
||||||
|
return (
|
||||||
|
<Fragment key={name}>
|
||||||
|
<tr key={name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||||
|
<td>
|
||||||
|
<CollapseToggle
|
||||||
|
isCollapsed={!expandedTemplates[name]}
|
||||||
|
onToggle={() => setExpandedTemplates({ ...expandedTemplates, [name]: !isExpanded })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{name}</td>
|
||||||
|
<td className={tableStyles.actionsCell}>
|
||||||
|
<ActionButton icon="pen">Edit</ActionButton>
|
||||||
|
<ActionIcon tooltip="delete template" icon="trash-alt" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isExpanded && (
|
||||||
|
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||||
|
<td></td>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<DetailsField label="Description" horizontal={true}>
|
||||||
|
<pre>{content}</pre>
|
||||||
|
</DetailsField>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ReceiversSection>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { Alert } from 'app/types/unified-alerting';
|
import { Alert } from 'app/types/unified-alerting';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Annotation } from '../Annotation';
|
import { Annotation } from '../Annotation';
|
||||||
import { DetailsField } from './DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instance: Alert;
|
instance: Alert;
|
||||||
|
@ -8,7 +8,7 @@ import { isCloudRulesSource } from '../../utils/datasource';
|
|||||||
import { Annotation } from '../Annotation';
|
import { Annotation } from '../Annotation';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
import { DetailsField } from './DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
import { RuleQuery } from './RuleQuery';
|
import { RuleQuery } from './RuleQuery';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ export const RulesTable: FC<Props> = ({
|
|||||||
<div className={wrapperClass}>
|
<div className={wrapperClass}>
|
||||||
<table className={tableStyles.table} data-testid="rules-table">
|
<table className={tableStyles.table} data-testid="rules-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className={styles.colExpand} />
|
<col className={tableStyles.colExpand} />
|
||||||
<col className={styles.colState} />
|
<col className={styles.colState} />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
@ -133,7 +133,7 @@ export const RulesTable: FC<Props> = ({
|
|||||||
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>
|
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>
|
||||||
)}
|
)}
|
||||||
<td>{statuses.join(', ') || 'n/a'}</td>
|
<td>{statuses.join(', ') || 'n/a'}</td>
|
||||||
<td className={styles.actionsCell}>
|
<td className={tableStyles.actionsCell}>
|
||||||
{isCloudRulesSource(rulesSource) && (
|
{isCloudRulesSource(rulesSource) && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="chart-line"
|
icon="chart-line"
|
||||||
@ -222,9 +222,6 @@ export const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
evenRow: css`
|
evenRow: css`
|
||||||
background-color: ${theme.colors.bodyBg};
|
background-color: ${theme.colors.bodyBg};
|
||||||
`,
|
`,
|
||||||
colExpand: css`
|
|
||||||
width: 36px;
|
|
||||||
`,
|
|
||||||
colState: css`
|
colState: css`
|
||||||
width: 110px;
|
width: 110px;
|
||||||
`,
|
`,
|
||||||
@ -254,13 +251,4 @@ export const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
top: -24px;
|
top: -24px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
`,
|
`,
|
||||||
actionsCell: css`
|
|
||||||
text-align: right;
|
|
||||||
width: 1%;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& > * + * {
|
|
||||||
margin-left: ${theme.spacing.sm};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
@ -23,4 +23,16 @@ export const getAlertTableStyles = (theme: GrafanaTheme) => ({
|
|||||||
evenRow: css`
|
evenRow: css`
|
||||||
background-color: ${theme.colors.bodyBg};
|
background-color: ${theme.colors.bodyBg};
|
||||||
`,
|
`,
|
||||||
|
colExpand: css`
|
||||||
|
width: 36px;
|
||||||
|
`,
|
||||||
|
actionsCell: css`
|
||||||
|
text-align: right;
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
& > * + * {
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -372,7 +372,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
{
|
{
|
||||||
path: '/alerting/notifications',
|
path: '/alerting/notifications',
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsListPage')
|
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user