Alerting: receivers page + template list (#33112)

This commit is contained in:
Domas 2021-04-19 13:02:58 +03:00 committed by GitHub
parent 382cab6406
commit 60b469f836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 207 additions and 28 deletions

View File

@ -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",

View 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;

View File

@ -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 && (

View 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;

View File

@ -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 && (

View File

@ -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 (

View File

@ -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}

View File

@ -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};
`,
});

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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';

View File

@ -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};
}
`,
}); });

View File

@ -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};
}
`,
}); });

View File

@ -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')
), ),
}, },
{ {