mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 01:16:31 -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() {
|
||||
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() {
|
||||
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{
|
||||
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 { useDispatch } from 'react-redux';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
@ -22,7 +22,9 @@ const AmRoutes: FC = () => {
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="am-routes">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
<Field label="Choose alert manager">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
</Field>
|
||||
<br />
|
||||
<br />
|
||||
{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 { useDispatch } from 'react-redux';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
@ -22,7 +22,9 @@ const Silences: FC = () => {
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="silences">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
<Field label="Choose alert manager">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
</Field>
|
||||
<br />
|
||||
<br />
|
||||
{error && !loading && (
|
||||
|
@ -19,7 +19,10 @@ export async function fetchAlertManagerConfig(alertmanagerSourceName: string): P
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.toPromise();
|
||||
return result.data;
|
||||
return {
|
||||
template_files: result.data.template_files ?? {},
|
||||
alertmanager_config: result.data.alertmanager_config ?? {},
|
||||
};
|
||||
} catch (e) {
|
||||
// if no config has been uploaded to grafana, it returns error instead of latest config
|
||||
if (
|
||||
|
@ -31,6 +31,7 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current }) => {
|
||||
|
||||
return (
|
||||
<Select
|
||||
width={29}
|
||||
className="ds-picker select-container"
|
||||
isMulti={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 React, { FC } from 'react';
|
||||
import { Annotation } from '../Annotation';
|
||||
import { DetailsField } from './DetailsField';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
|
||||
interface Props {
|
||||
instance: Alert;
|
||||
|
@ -8,7 +8,7 @@ import { isCloudRulesSource } from '../../utils/datasource';
|
||||
import { Annotation } from '../Annotation';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||
import { DetailsField } from './DetailsField';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
import { RuleQuery } from './RuleQuery';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export const RulesTable: FC<Props> = ({
|
||||
<div className={wrapperClass}>
|
||||
<table className={tableStyles.table} data-testid="rules-table">
|
||||
<colgroup>
|
||||
<col className={styles.colExpand} />
|
||||
<col className={tableStyles.colExpand} />
|
||||
<col className={styles.colState} />
|
||||
<col />
|
||||
<col />
|
||||
@ -133,7 +133,7 @@ export const RulesTable: FC<Props> = ({
|
||||
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>
|
||||
)}
|
||||
<td>{statuses.join(', ') || 'n/a'}</td>
|
||||
<td className={styles.actionsCell}>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
{isCloudRulesSource(rulesSource) && (
|
||||
<ActionIcon
|
||||
icon="chart-line"
|
||||
@ -222,9 +222,6 @@ export const getStyles = (theme: GrafanaTheme) => ({
|
||||
evenRow: css`
|
||||
background-color: ${theme.colors.bodyBg};
|
||||
`,
|
||||
colExpand: css`
|
||||
width: 36px;
|
||||
`,
|
||||
colState: css`
|
||||
width: 110px;
|
||||
`,
|
||||
@ -254,13 +251,4 @@ export const getStyles = (theme: GrafanaTheme) => ({
|
||||
top: -24px;
|
||||
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`
|
||||
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',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsListPage')
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
|
||||
),
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user