Alerting: miscllaneous UI fixes & improvements (#33734)

This commit is contained in:
Domas
2021-05-06 11:21:58 +03:00
committed by GitHub
parent d994d0e762
commit d2d13ea39a
27 changed files with 217 additions and 157 deletions

View File

@@ -1,7 +1,7 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { Receiver } from 'app/plugins/datasource/alertmanager/types'; import { Receiver } from 'app/plugins/datasource/alertmanager/types';
@@ -96,9 +96,7 @@ const AmRoutes: FC = () => {
return ( return (
<AlertingPageWrapper pageId="am-routes"> <AlertingPageWrapper pageId="am-routes">
<Field label="Choose alert manager">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} /> <AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
</Field>
{savingError && !saving && ( {savingError && !saving && (
<Alert severity="error" title="Error saving alert manager config"> <Alert severity="error" title="Error saving alert manager config">
{savingError.message || 'Unknown error.'} {savingError.message || 'Unknown error.'}
@@ -112,7 +110,6 @@ const AmRoutes: FC = () => {
{resultLoading && <LoadingPlaceholder text="Loading alert manager config..." />} {resultLoading && <LoadingPlaceholder text="Loading alert manager config..." />}
{result && !resultLoading && !resultError && ( {result && !resultLoading && !resultError && (
<> <>
<div className={styles.break} />
<AmRootRoute <AmRootRoute
alertManagerSourceName={alertManagerSourceName} alertManagerSourceName={alertManagerSourceName}
isEditMode={isRootRouteEditMode} isEditMode={isRootRouteEditMode}

View File

@@ -1,4 +1,4 @@
import { Field, Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, 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 { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
@@ -50,9 +50,11 @@ const Receivers: FC = () => {
return ( return (
<AlertingPageWrapper pageId="receivers"> <AlertingPageWrapper pageId="receivers">
<Field label={disableAmSelect ? 'Alert manager' : 'Choose alert manager'} disabled={disableAmSelect}> <AlertManagerPicker
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} /> current={alertManagerSourceName}
</Field> disabled={disableAmSelect}
onChange={setAlertManagerSourceName}
/>
{error && !loading && ( {error && !loading && (
<Alert severity="error" title="Error loading alert manager config"> <Alert severity="error" title="Error loading alert manager config">
{error.message || 'Unknown error.'} {error.message || 'Unknown error.'}

View File

@@ -2,23 +2,29 @@ import React, { FC, useEffect, useCallback } from 'react';
import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Redirect, Route, RouteChildrenProps, Switch } from 'react-router-dom'; import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import SilencesTable from './components/silences/SilencesTable'; import SilencesTable from './components/silences/SilencesTable';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions'; import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions';
import { SILENCES_POLL_INTERVAL_MS } from './utils/constants'; import { SILENCES_POLL_INTERVAL_MS } from './utils/constants';
import { initialAsyncRequestState } from './utils/redux'; import { AsyncRequestState, initialAsyncRequestState } from './utils/redux';
import SilencesEditor from './components/silences/SilencesEditor'; import SilencesEditor from './components/silences/SilencesEditor';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { Silence } from 'app/plugins/datasource/alertmanager/types';
const Silences: FC = () => { const Silences: FC = () => {
const [alertManagerSourceName = '', setAlertManagerSourceName] = useAlertManagerSourceName(); const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const dispatch = useDispatch(); const dispatch = useDispatch();
const silences = useUnifiedAlertingSelector((state) => state.silences); const silences = useUnifiedAlertingSelector((state) => state.silences);
const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts);
const alertsRequest = alertManagerSourceName
? alertsRequests[alertManagerSourceName] || initialAsyncRequestState
: undefined;
const alerts = const location = useLocation();
useUnifiedAlertingSelector((state) => state.amAlerts)[alertManagerSourceName] || initialAsyncRequestState; const isRoot = location.pathname.endsWith('/alerting/silences');
useEffect(() => { useEffect(() => {
function fetchAll() { function fetchAll() {
@@ -34,7 +40,9 @@ const Silences: FC = () => {
}; };
}, [alertManagerSourceName, dispatch]); }, [alertManagerSourceName, dispatch]);
const { result, loading, error } = silences[alertManagerSourceName] || initialAsyncRequestState; const { result, loading, error }: AsyncRequestState<Silence[]> =
(alertManagerSourceName && silences[alertManagerSourceName]) || initialAsyncRequestState;
const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]);
if (!alertManagerSourceName) { if (!alertManagerSourceName) {
@@ -43,20 +51,20 @@ const Silences: FC = () => {
return ( return (
<AlertingPageWrapper pageId="silences"> <AlertingPageWrapper pageId="silences">
<AlertManagerPicker disabled={!isRoot} current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
{error && !loading && ( {error && !loading && (
<Alert severity="error" title="Error loading silences"> <Alert severity="error" title="Error loading silences">
{error.message || 'Unknown error.'} {error.message || 'Unknown error.'}
</Alert> </Alert>
)} )}
{loading && <LoadingPlaceholder text="loading silences..." />} {loading && <LoadingPlaceholder text="loading silences..." />}
{result && !error && alerts.result && ( {result && !error && alertsRequest?.result && (
<Switch> <Switch>
<Route exact path="/alerting/silences"> <Route exact path="/alerting/silences">
<SilencesTable <SilencesTable
silences={result} silences={result}
alertManagerAlerts={alerts.result} alertManagerAlerts={alertsRequest.result}
alertManagerSourceName={alertManagerSourceName} alertManagerSourceName={alertManagerSourceName}
setAlertManagerSourceName={setAlertManagerSourceName}
/> />
</Route> </Route>
<Route exact path="/alerting/silence/new"> <Route exact path="/alerting/silence/new">

View File

@@ -1,8 +1,9 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue, GrafanaTheme2 } from '@grafana/data';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { Select } from '@grafana/ui'; import { Field, Select, useStyles2 } from '@grafana/ui';
import { getAllDataSources } from '../utils/config'; import { getAllDataSources } from '../utils/config';
import { css } from '@emotion/css';
interface Props { interface Props {
onChange: (alertManagerSourceName: string) => void; onChange: (alertManagerSourceName: string) => void;
@@ -11,6 +12,8 @@ interface Props {
} }
export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = false }) => { export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = false }) => {
const styles = useStyles2(getStyles);
const options: Array<SelectableValue<string>> = useMemo(() => { const options: Array<SelectableValue<string>> = useMemo(() => {
return [ return [
{ {
@@ -30,13 +33,16 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = fa
]; ];
}, []); }, []);
// no need to show the picker if there's only one option
if (options.length === 1) {
return null;
}
return ( return (
<Field className={styles.field} label={disabled ? 'Alert manager' : 'Choose alert manager'} disabled={disabled}>
<Select <Select
disabled={disabled}
width={29} width={29}
className="ds-picker select-container" className="ds-picker select-container"
isMulti={false}
isClearable={false}
backspaceRemovesValue={false} backspaceRemovesValue={false}
onChange={(value) => value.value && onChange(value.value)} onChange={(value) => value.value && onChange(value.value)}
options={options} options={options}
@@ -45,5 +51,12 @@ export const AlertManagerPicker: FC<Props> = ({ onChange, current, disabled = fa
value={current} value={current}
getOptionLabel={(o) => o.label} getOptionLabel={(o) => o.label}
/> />
</Field>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
field: css`
margin-bottom: ${theme.spacing(4)};
`,
});

View File

@@ -1,18 +1,18 @@
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { SilenceState, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { FC } from 'react'; import React, { FC } from 'react';
export type State = 'good' | 'bad' | 'warning' | 'neutral' | 'info';
type Props = { type Props = {
status: PromAlertingRuleState | SilenceState | AlertState; state: State;
}; };
export const StateTag: FC<Props> = ({ children, status }) => { export const StateTag: FC<Props> = ({ children, state }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return <span className={cx(styles.common, styles[status])}>{children || status}</span>; return <span className={cx(styles.common, styles[state])}>{children || state}</span>;
}; };
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
@@ -25,37 +25,27 @@ const getStyles = (theme: GrafanaTheme2) => ({
text-transform: capitalize; text-transform: capitalize;
line-height: 1.2; line-height: 1.2;
`, `,
[PromAlertingRuleState.Inactive]: css` good: css`
background-color: ${theme.colors.success.main}; background-color: ${theme.colors.success.main};
border: solid 1px ${theme.colors.success.main}; border: solid 1px ${theme.colors.success.main};
color: ${theme.colors.success.contrastText}; color: ${theme.colors.success.contrastText};
`, `,
[PromAlertingRuleState.Pending]: css` warning: css`
background-color: ${theme.colors.warning.main}; background-color: ${theme.colors.warning.main};
border: solid 1px ${theme.colors.warning.main}; border: solid 1px ${theme.colors.warning.main};
color: ${theme.colors.warning.contrastText}; color: ${theme.colors.warning.contrastText};
`, `,
[PromAlertingRuleState.Firing]: css` bad: css`
background-color: ${theme.colors.error.main}; background-color: ${theme.colors.error.main};
border: solid 1px ${theme.colors.error.main}; border: solid 1px ${theme.colors.error.main};
color: ${theme.colors.error.contrastText}; color: ${theme.colors.error.contrastText};
`, `,
[SilenceState.Expired]: css` neutral: css`
background-color: ${theme.colors.secondary.main}; background-color: ${theme.colors.secondary.main};
border: solid 1px ${theme.colors.secondary.main}; border: solid 1px ${theme.colors.secondary.main};
color: ${theme.colors.secondary.contrastText}; color: ${theme.colors.secondary.contrastText};
`, `,
[SilenceState.Active]: css` info: css`
background-color: ${theme.colors.success.main};
border: solid 1px ${theme.colors.success.main};
color: ${theme.colors.success.contrastText};
`,
[AlertState.Unprocessed]: css`
background-color: ${theme.colors.secondary.main};
border: solid 1px ${theme.colors.secondary.main};
color: ${theme.colors.secondary.contrastText};
`,
[AlertState.Suppressed]: css`
background-color: ${theme.colors.primary.main}; background-color: ${theme.colors.primary.main};
border: solid 1px ${theme.colors.primary.main}; border: solid 1px ${theme.colors.primary.main};
color: ${theme.colors.primary.contrastText}; color: ${theme.colors.primary.contrastText};

View File

@@ -187,7 +187,7 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
</Collapse> </Collapse>
<div className={styles.container}> <div className={styles.container}>
<Button type="submit">Save</Button> <Button type="submit">Save</Button>
<Button onClick={onCancel} type="reset" variant="secondary"> <Button onClick={onCancel} type="reset" variant="secondary" fill="outline">
Cancel Cancel
</Button> </Button>
</div> </div>

View File

@@ -257,7 +257,7 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
)} )}
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<Button type="submit">Save policy</Button> <Button type="submit">Save policy</Button>
<Button onClick={onCancel} type="button" variant="secondary"> <Button onClick={onCancel} fill="outline" type="button" variant="secondary">
Cancel Cancel
</Button> </Button>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import React, { FC } from 'react'; import React, { FC } from 'react';
interface Props { interface Props {
@@ -8,13 +8,21 @@ interface Props {
description: string; description: string;
addButtonLabel: string; addButtonLabel: string;
addButtonTo: string; addButtonTo: string;
className?: string;
} }
export const ReceiversSection: FC<Props> = ({ title, description, addButtonLabel, addButtonTo, children }) => { export const ReceiversSection: FC<Props> = ({
const styles = useStyles(getStyles); className,
title,
description,
addButtonLabel,
addButtonTo,
children,
}) => {
const styles = useStyles2(getStyles);
return ( return (
<> <>
<div className={styles.heading}> <div className={cx(styles.heading, className)}>
<div> <div>
<h4>{title}</h4> <h4>{title}</h4>
<p className={styles.description}>{description}</p> <p className={styles.description}>{description}</p>
@@ -28,13 +36,12 @@ export const ReceiversSection: FC<Props> = ({ title, description, addButtonLabel
); );
}; };
const getStyles = (theme: GrafanaTheme) => ({ const getStyles = (theme: GrafanaTheme2) => ({
heading: css` heading: css`
margin-top: ${theme.spacing.xl};
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
`, `,
description: css` description: css`
color: ${theme.colors.textSemiWeak}; color: ${theme.colors.text.secondary};
`, `,
}); });

View File

@@ -7,6 +7,8 @@ import { extractReadableNotifierTypes } from '../../utils/receivers';
import { ActionIcon } from '../rules/ActionIcon'; import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection'; import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
interface Props { interface Props {
config: AlertManagerCortexConfig; config: AlertManagerCortexConfig;
@@ -15,6 +17,7 @@ interface Props {
export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => { export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
const styles = useStyles2(getStyles);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
@@ -29,6 +32,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
return ( return (
<ReceiversSection <ReceiversSection
className={styles.section}
title="Contact points" title="Contact points"
description="Define where the notifications will be sent to, for example email or Slack." description="Define where the notifications will be sent to, for example email or Slack."
addButtonLabel="New contact point" addButtonLabel="New contact point"
@@ -75,3 +79,9 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
</ReceiversSection> </ReceiversSection>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
section: css`
margin-top: ${theme.spacing(4)};
`,
});

View File

@@ -153,6 +153,7 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
href={makeAMLink('alerting/notifications', alertManagerSourceName)} href={makeAMLink('alerting/notifications', alertManagerSourceName)}
variant="secondary" variant="secondary"
type="button" type="button"
fill="outline"
> >
Cancel Cancel
</LinkButton> </LinkButton>

View File

@@ -120,7 +120,8 @@ export function ReceiverForm<R extends ChannelValues>({
<LinkButton <LinkButton
disabled={loading} disabled={loading}
variant="secondary" variant="secondary"
href={makeAMLink('/alerting/notifications', alertManagerSourceName)} fill="outline"
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
> >
Cancel Cancel
</LinkButton> </LinkButton>

View File

@@ -1,16 +1,16 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui'; import { Select } from '@grafana/ui';
import { SelectBaseProps } from '@grafana/ui/src/components/Select/types'; import { SelectBaseProps } from '@grafana/ui/src/components/Select/types';
import { GrafanaAlertState } from 'app/types/unified-alerting-dto'; import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
import React, { FC } from 'react'; import React, { FC } from 'react';
type Props = Omit<SelectBaseProps<GrafanaAlertState>, 'options'>; type Props = Omit<SelectBaseProps<GrafanaAlertStateDecision>, 'options'>;
const options: SelectableValue[] = [ const options: SelectableValue[] = [
{ value: GrafanaAlertState.Alerting, label: 'Alerting' }, { value: GrafanaAlertStateDecision.Alerting, label: 'Alerting' },
{ value: GrafanaAlertState.NoData, label: 'No Data' }, { value: GrafanaAlertStateDecision.NoData, label: 'No Data' },
{ value: GrafanaAlertState.KeepLastState, label: 'Keep Last State' }, { value: GrafanaAlertStateDecision.KeepLastState, label: 'Keep Last State' },
{ value: GrafanaAlertState.OK, label: 'OK' }, { value: GrafanaAlertStateDecision.OK, label: 'OK' },
]; ];
export const GrafanaAlertStatePicker: FC<Props> = (props) => <Select options={options} {...props} />; export const GrafanaAlertStatePicker: FC<Props> = (props) => <Select options={options} {...props} />;

View File

@@ -12,9 +12,11 @@ export const AlertInstanceDetails: FC<Props> = ({ instance }) => {
return ( return (
<div> <div>
{instance.value && (
<DetailsField label="Value" horizontal={true}> <DetailsField label="Value" horizontal={true}>
{instance.value} {instance.value}
</DetailsField> </DetailsField>
)}
{annotations.map(([key, value]) => ( {annotations.map(([key, value]) => (
<DetailsField key={key} label={key} horizontal={true}> <DetailsField key={key} label={key} horizontal={true}>
<Annotation annotationKey={key} value={value} /> <Annotation annotationKey={key} value={value} />

View File

@@ -7,8 +7,8 @@ import { getAlertTableStyles } from '../../styles/table';
import { alertInstanceKey } from '../../utils/rules'; import { alertInstanceKey } from '../../utils/rules';
import { AlertLabels } from '../AlertLabels'; import { AlertLabels } from '../AlertLabels';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { StateTag } from '../StateTag';
import { AlertInstanceDetails } from './AlertInstanceDetails'; import { AlertInstanceDetails } from './AlertInstanceDetails';
import { AlertStateTag } from './AlertStateTag';
interface Props { interface Props {
instances: AlertingRule['alerts']; instances: AlertingRule['alerts'];
@@ -45,25 +45,32 @@ export const AlertInstancesTable: FC<Props> = ({ instances }) => {
{instances.map((instance, idx) => { {instances.map((instance, idx) => {
const key = alertInstanceKey(instance); const key = alertInstanceKey(instance);
const isExpanded = expandedKeys.includes(key); const isExpanded = expandedKeys.includes(key);
// don't allow expanding if there's nothing to show
const isExpandable = instance.value || !!Object.keys(instance.annotations ?? {}).length;
return ( return (
<Fragment key={key}> <Fragment key={key}>
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}> <tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
<td> <td>
{isExpandable && (
<CollapseToggle <CollapseToggle
isCollapsed={!isExpanded} isCollapsed={!isExpanded}
onToggle={() => toggleExpandedState(key)} onToggle={() => toggleExpandedState(key)}
data-testid="alert-collapse-toggle" data-testid="alert-collapse-toggle"
/> />
)}
</td> </td>
<td> <td>
<StateTag status={instance.state} /> <AlertStateTag state={instance.state} />
</td> </td>
<td className={styles.labelsCell}> <td className={styles.labelsCell}>
<AlertLabels labels={instance.labels} /> <AlertLabels labels={instance.labels} />
</td> </td>
<td className={styles.createdCell}>{instance.activeAt.substr(0, 19).replace('T', ' ')}</td> <td className={styles.createdCell}>
{instance.activeAt.startsWith('0001') ? '-' : instance.activeAt.substr(0, 19).replace('T', ' ')}
</td>
</tr> </tr>
{isExpanded && ( {isExpanded && isExpandable && (
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}> <tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
<td></td> <td></td>
<td colSpan={3}> <td colSpan={3}>

View File

@@ -0,0 +1,20 @@
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC } from 'react';
import { State, StateTag } from '../StateTag';
const alertStateToState: Record<PromAlertingRuleState | GrafanaAlertState, State> = {
[PromAlertingRuleState.Inactive]: 'good',
[PromAlertingRuleState.Firing]: 'bad',
[PromAlertingRuleState.Pending]: 'warning',
[GrafanaAlertState.Alerting]: 'bad',
[GrafanaAlertState.Error]: 'bad',
[GrafanaAlertState.NoData]: 'info',
[GrafanaAlertState.Normal]: 'good',
[GrafanaAlertState.Pending]: 'warning',
};
interface Props {
state: PromAlertingRuleState | GrafanaAlertState;
}
export const AlertStateTag: FC<Props> = ({ state }) => <StateTag state={alertStateToState[state]}>{state}</StateTag>;

View File

@@ -4,14 +4,14 @@ import { useStyles } from '@grafana/ui';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules'; import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; 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 { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
import { Expression } from '../Expression';
interface Props { interface Props {
rule: CombinedRule; rule: CombinedRule;
@@ -57,13 +57,15 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
<AlertLabels labels={rule.labels} /> <AlertLabels labels={rule.labels} />
</DetailsField> </DetailsField>
)} )}
{isCloudRulesSource(rulesSource) && (
<DetailsField <DetailsField
label={isGrafanaRulesSource(rulesSource) ? 'Query' : 'Expression'} label="Expression"
className={cx({ [styles.exprRow]: !!annotations.length })} className={cx({ [styles.exprRow]: !!annotations.length })}
horizontal={true} horizontal={true}
> >
<RuleQuery rule={rule} rulesSource={rulesSource} /> <Expression expression={rule.query} rulesSource={rulesSource} />
</DetailsField> </DetailsField>
)}
{annotations.map(([key, value]) => ( {annotations.map(([key, value]) => (
<DetailsField key={key} label={key} horizontal={true}> <DetailsField key={key} label={key} horizontal={true}>
<Annotation annotationKey={key} value={value} /> <Annotation annotationKey={key} value={value} />

View File

@@ -1,34 +0,0 @@
import { useTheme2 } from '@grafana/ui';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import React, { FC, useState } from 'react';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { isGrafanaRulerRule } from '../../utils/rules';
import { Expression } from '../Expression';
interface Props {
rule: CombinedRule;
rulesSource: RulesSource;
}
export const RuleQuery: FC<Props> = ({ rule, rulesSource }) => {
const { rulerRule } = rule;
const theme = useTheme2();
const [isHidden, setIsHidden] = useState(true);
if (rulesSource !== GRAFANA_RULES_SOURCE_NAME) {
return <Expression expression={rule.query} rulesSource={rulesSource} />;
}
if (rulerRule && isGrafanaRulerRule(rulerRule)) {
// @TODO: better grafana queries vizualization read-only
if (isHidden) {
return (
<a style={{ color: theme.colors.text.link }} onClick={() => setIsHidden(false)}>
Show raw query JSON
</a>
);
}
return <pre>{JSON.stringify(rulerRule.grafana_alert.data, null, 2)}</pre>;
}
return <pre>@TODO: handle grafana prom rule case</pre>;
};

View File

@@ -4,7 +4,6 @@ import React, { FC, Fragment, useState } from 'react';
import { getRuleIdentifier, isAlertingRule, stringifyRuleIdentifier } from '../../utils/rules'; import { getRuleIdentifier, isAlertingRule, stringifyRuleIdentifier } from '../../utils/rules';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { StateTag } from '../StateTag';
import { RuleDetails } from './RuleDetails'; import { RuleDetails } from './RuleDetails';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
import { ActionIcon } from './ActionIcon'; import { ActionIcon } from './ActionIcon';
@@ -14,6 +13,7 @@ import { useDispatch } from 'react-redux';
import { deleteRuleAction } from '../../state/actions'; import { deleteRuleAction } from '../../state/actions';
import { useHasRuler } from '../../hooks/useHasRuler'; import { useHasRuler } from '../../hooks/useHasRuler';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { AlertStateTag } from './AlertStateTag';
interface Props { interface Props {
rules: CombinedRule[]; rules: CombinedRule[];
@@ -63,7 +63,7 @@ export const RulesTable: FC<Props> = ({
const wrapperClass = cx(styles.wrapper, { [styles.wrapperMargin]: showGuidelines }); const wrapperClass = cx(styles.wrapper, { [styles.wrapperMargin]: showGuidelines });
if (!rules.length) { if (!rules.length) {
return <div className={wrapperClass}>{emptyMessage}</div>; return <div className={cx(wrapperClass, styles.emptyMessage)}>{emptyMessage}</div>;
} }
return ( return (
@@ -126,7 +126,7 @@ export const RulesTable: FC<Props> = ({
data-testid="rule-collapse-toggle" data-testid="rule-collapse-toggle"
/> />
</td> </td>
<td>{promRule && isAlertingRule(promRule) ? <StateTag status={promRule.state} /> : 'n/a'}</td> <td>{promRule && isAlertingRule(promRule) ? <AlertStateTag state={promRule.state} /> : 'n/a'}</td>
<td>{rule.name}</td> <td>{rule.name}</td>
{showGroupColumn && ( {showGroupColumn && (
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td> <td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td>
@@ -194,6 +194,9 @@ export const getStyles = (theme: GrafanaTheme2) => ({
wrapperMargin: css` wrapperMargin: css`
margin-left: 36px; margin-left: 36px;
`, `,
emptyMessage: css`
padding: ${theme.spacing(1)};
`,
wrapper: css` wrapper: css`
margin-top: ${theme.spacing(3)}; margin-top: ${theme.spacing(3)};
width: auto; width: auto;

View File

@@ -0,0 +1,15 @@
import { AlertState } from 'app/plugins/datasource/alertmanager/types';
import React, { FC } from 'react';
import { State, StateTag } from '../StateTag';
const alertStateToState: Record<AlertState, State> = {
[AlertState.Active]: 'bad',
[AlertState.Unprocessed]: 'neutral',
[AlertState.Suppressed]: 'info',
};
interface Props {
state: AlertState;
}
export const AmAlertStateTag: FC<Props> = ({ state }) => <StateTag state={alertStateToState[state]}>{state}</StateTag>;

View File

@@ -0,0 +1,17 @@
import { SilenceState } from 'app/plugins/datasource/alertmanager/types';
import React, { FC } from 'react';
import { State, StateTag } from '../StateTag';
const silenceStateToState: Record<SilenceState, State> = {
[SilenceState.Active]: 'good',
[SilenceState.Expired]: 'neutral',
[SilenceState.Pending]: 'neutral',
};
interface Props {
state: SilenceState;
}
export const SilenceStateTag: FC<Props> = ({ state }) => (
<StateTag state={silenceStateToState[state]}>{state}</StateTag>
);

View File

@@ -2,7 +2,6 @@ import React, { FC, Fragment, useState } from 'react';
import { dateMath, GrafanaTheme, toDuration } from '@grafana/data'; import { dateMath, GrafanaTheme, toDuration } from '@grafana/data';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { Silence, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; import { Silence, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
import { StateTag } from '../StateTag';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { ActionButton } from '../rules/ActionButton'; import { ActionButton } from '../rules/ActionButton';
import { ActionIcon } from '../rules/ActionIcon'; import { ActionIcon } from '../rules/ActionIcon';
@@ -11,6 +10,7 @@ import SilencedAlertsTable from './SilencedAlertsTable';
import { expireSilenceAction } from '../../state/actions'; import { expireSilenceAction } from '../../state/actions';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Matchers } from './Matchers'; import { Matchers } from './Matchers';
import { SilenceStateTag } from './SilenceStateTag';
interface Props { interface Props {
className?: string; className?: string;
silence: Silence; silence: Silence;
@@ -41,7 +41,7 @@ const SilenceTableRow: FC<Props> = ({ silence, className, silencedAlerts, alertM
<CollapseToggle isCollapsed={isCollapsed} onToggle={(value) => setIsCollapsed(value)} /> <CollapseToggle isCollapsed={isCollapsed} onToggle={(value) => setIsCollapsed(value)} />
</td> </td>
<td> <td>
<StateTag status={status.state}>{status.state}</StateTag> <SilenceStateTag state={status.state} />
</td> </td>
<td className={styles.matchersCell}> <td className={styles.matchersCell}>
<Matchers matchers={matchers} /> <Matchers matchers={matchers} />

View File

@@ -1,12 +1,12 @@
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { CollapseToggle } from '../CollapseToggle'; import { CollapseToggle } from '../CollapseToggle';
import { StateTag } from '../StateTag';
import { ActionIcon } from '../rules/ActionIcon'; import { ActionIcon } from '../rules/ActionIcon';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { dateTimeAsMoment, toDuration } from '@grafana/data'; import { dateTimeAsMoment, toDuration } from '@grafana/data';
import { AlertLabels } from '../AlertLabels'; import { AlertLabels } from '../AlertLabels';
import { AmAlertStateTag } from './AmAlertStateTag';
interface Props { interface Props {
alert: AlertmanagerAlert; alert: AlertmanagerAlert;
@@ -30,7 +30,7 @@ export const SilencedAlertsTableRow: FC<Props> = ({ alert, className }) => {
<CollapseToggle isCollapsed={isCollapsed} onToggle={(collapsed) => setIsCollapsed(collapsed)} /> <CollapseToggle isCollapsed={isCollapsed} onToggle={(collapsed) => setIsCollapsed(collapsed)} />
</td> </td>
<td> <td>
<StateTag status={alert.status.state}>{alert.status.state}</StateTag> <AmAlertStateTag state={alert.status.state} />
</td> </td>
<td>for {alertDuration} seconds</td> <td>for {alertDuration} seconds</td>
<td>{alertName}</td> <td>{alertName}</td>

View File

@@ -1,26 +1,19 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2, Link, Button, Field } from '@grafana/ui'; import { Icon, useStyles2, Link, Button } from '@grafana/ui';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { AlertmanagerAlert, Silence } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert, Silence } from 'app/plugins/datasource/alertmanager/types';
import SilenceTableRow from './SilenceTableRow'; import SilenceTableRow from './SilenceTableRow';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
import { NoSilencesSplash } from './NoSilencesCTA'; import { NoSilencesSplash } from './NoSilencesCTA';
import { AlertManagerPicker } from '../AlertManagerPicker';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
interface Props { interface Props {
silences: Silence[]; silences: Silence[];
alertManagerAlerts: AlertmanagerAlert[]; alertManagerAlerts: AlertmanagerAlert[];
alertManagerSourceName: string; alertManagerSourceName: string;
setAlertManagerSourceName(name: string): void;
} }
const SilencesTable: FC<Props> = ({ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSourceName }) => {
silences,
alertManagerAlerts,
alertManagerSourceName,
setAlertManagerSourceName,
}) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
@@ -30,9 +23,6 @@ const SilencesTable: FC<Props> = ({
return ( return (
<> <>
<Field label="Choose alert manager">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
</Field>
{!!silences.length && ( {!!silences.length && (
<> <>
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}> <Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>

View File

@@ -1,4 +1,4 @@
import { GrafanaQuery, GrafanaAlertState } from 'app/types/unified-alerting-dto'; import { GrafanaQuery, GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
export enum RuleFormType { export enum RuleFormType {
threshold = 'threshold', threshold = 'threshold',
@@ -17,8 +17,8 @@ export interface RuleFormValues {
// threshold alerts // threshold alerts
queries: GrafanaQuery[]; queries: GrafanaQuery[];
condition: string | null; // refId of the query that gets alerted on condition: string | null; // refId of the query that gets alerted on
noDataState: GrafanaAlertState; noDataState: GrafanaAlertStateDecision;
execErrState: GrafanaAlertState; execErrState: GrafanaAlertStateDecision;
folder: { title: string; id: number } | null; folder: { title: string; id: number } | null;
evaluateEvery: string; evaluateEvery: string;
evaluateFor: string; evaluateFor: string;

View File

@@ -5,7 +5,7 @@ import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/t
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
import { import {
Annotations, Annotations,
GrafanaAlertState, GrafanaAlertStateDecision,
GrafanaQuery, GrafanaQuery,
Labels, Labels,
PostableRuleGrafanaRuleDTO, PostableRuleGrafanaRuleDTO,
@@ -28,8 +28,8 @@ export const defaultFormValues: RuleFormValues = Object.freeze({
folder: null, folder: null,
queries: [], queries: [],
condition: '', condition: '',
noDataState: GrafanaAlertState.NoData, noDataState: GrafanaAlertStateDecision.NoData,
execErrState: GrafanaAlertState.Alerting, execErrState: GrafanaAlertStateDecision.Alerting,
evaluateEvery: '1m', evaluateEvery: '1m',
evaluateFor: '5m', evaluateFor: '5m',

View File

@@ -11,6 +11,14 @@ export enum PromAlertingRuleState {
Pending = 'pending', Pending = 'pending',
} }
export enum GrafanaAlertState {
Normal = 'Normal',
Alerting = 'Alerting',
Pending = 'Pending',
NoData = 'NoData',
Error = 'Error',
}
export enum PromRuleType { export enum PromRuleType {
Alerting = 'alerting', Alerting = 'alerting',
Recording = 'recording', Recording = 'recording',
@@ -29,7 +37,7 @@ export interface PromAlertingRuleDTO extends PromRuleDTOBase {
alerts: Array<{ alerts: Array<{
labels: Labels; labels: Labels;
annotations: Annotations; annotations: Annotations;
state: Exclude<PromAlertingRuleState, PromAlertingRuleState.Inactive>; state: Exclude<PromAlertingRuleState | GrafanaAlertState, PromAlertingRuleState.Inactive>;
activeAt: string; activeAt: string;
value: string; value: string;
}>; }>;
@@ -86,7 +94,7 @@ export interface RulerAlertingRuleDTO extends RulerRuleBaseDTO {
annotations?: Annotations; annotations?: Annotations;
} }
export enum GrafanaAlertState { export enum GrafanaAlertStateDecision {
Alerting = 'Alerting', Alerting = 'Alerting',
NoData = 'NoData', NoData = 'NoData',
KeepLastState = 'KeepLastState', KeepLastState = 'KeepLastState',
@@ -104,8 +112,8 @@ export interface PostableGrafanaRuleDefinition {
uid?: string; uid?: string;
title: string; title: string;
condition: string; condition: string;
no_data_state: GrafanaAlertState; no_data_state: GrafanaAlertStateDecision;
exec_err_state: GrafanaAlertState; exec_err_state: GrafanaAlertStateDecision;
data: GrafanaQuery[]; data: GrafanaQuery[];
} }
export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition { export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {

View File

@@ -8,13 +8,14 @@ import {
Labels, Labels,
Annotations, Annotations,
RulerRuleGroupDTO, RulerRuleGroupDTO,
GrafanaAlertState,
} from './unified-alerting-dto'; } from './unified-alerting-dto';
export type Alert = { export type Alert = {
activeAt: string; activeAt: string;
annotations: { [key: string]: string }; annotations: { [key: string]: string };
labels: { [key: string]: string }; labels: { [key: string]: string };
state: PromAlertingRuleState; state: PromAlertingRuleState | GrafanaAlertState;
value: string; value: string;
}; };