mirror of https://github.com/grafana/grafana
Alerting: New alert list panel component (#34614)
parent
7c25465b3a
commit
7dd5a065ba
@ -0,0 +1,78 @@ |
||||
import React, { useEffect, useMemo, useState } from 'react'; |
||||
import pluralize from 'pluralize'; |
||||
import { Icon, useStyles2 } from '@grafana/ui'; |
||||
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting'; |
||||
import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels'; |
||||
import { AlertStateTag } from 'app/features/alerting/unified/components/rules/AlertStateTag'; |
||||
import { dateTime, GrafanaTheme2 } from '@grafana/data'; |
||||
import { css } from '@emotion/css'; |
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; |
||||
import { omit } from 'lodash'; |
||||
import { alertInstanceKey } from 'app/features/alerting/unified/utils/rules'; |
||||
|
||||
interface Props { |
||||
ruleWithLocation: PromRuleWithLocation; |
||||
showInstances: boolean; |
||||
} |
||||
|
||||
export const AlertInstances = ({ ruleWithLocation, showInstances }: Props) => { |
||||
const { rule } = ruleWithLocation; |
||||
const [displayInstances, setDisplayInstances] = useState<boolean>(showInstances); |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
useEffect(() => { |
||||
setDisplayInstances(showInstances); |
||||
}, [showInstances]); |
||||
|
||||
// sort instances, because API returns them in random order every time
|
||||
const sortedAlerts = useMemo( |
||||
(): Alert[] => |
||||
displayInstances |
||||
? rule.alerts.slice().sort((a, b) => alertInstanceKey(a).localeCompare(alertInstanceKey(b))) |
||||
: [], |
||||
[rule, displayInstances] |
||||
); |
||||
|
||||
return ( |
||||
<div> |
||||
{rule.state !== PromAlertingRuleState.Inactive && ( |
||||
<div className={styles.instance} onClick={() => setDisplayInstances(!displayInstances)}> |
||||
<Icon name={displayInstances ? 'angle-down' : 'angle-right'} size={'md'} /> |
||||
<span>{`${rule.alerts.length} ${pluralize('instance', rule.alerts.length)}`}</span> |
||||
</div> |
||||
)} |
||||
|
||||
{!!sortedAlerts.length && ( |
||||
<ol className={styles.list}> |
||||
{sortedAlerts.map((alert, index) => { |
||||
return ( |
||||
<li className={styles.listItem} key={`${alert.activeAt}-${index}`}> |
||||
<div> |
||||
<AlertStateTag state={alert.state} /> |
||||
<span className={styles.date}>{dateTime(alert.activeAt).format('YYYY-MM-DD HH:mm:ss')}</span> |
||||
</div> |
||||
<AlertLabels labels={omit(alert.labels, 'alertname')} /> |
||||
</li> |
||||
); |
||||
})} |
||||
</ol> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
instance: css` |
||||
cursor: pointer; |
||||
`,
|
||||
list: css` |
||||
list-style-type: none; |
||||
`,
|
||||
listItem: css` |
||||
margin-top: ${theme.spacing(1)}; |
||||
`,
|
||||
date: css` |
||||
font-size: ${theme.typography.bodySmall.fontSize}; |
||||
padding-left: ${theme.spacing(0.5)}; |
||||
`,
|
||||
}); |
@ -0,0 +1,296 @@ |
||||
import React, { useEffect, useMemo } from 'react'; |
||||
import { sortBy } from 'lodash'; |
||||
import { useDispatch } from 'react-redux'; |
||||
import { GrafanaTheme, GrafanaTheme2, intervalToAbbreviatedDurationString, PanelProps } from '@grafana/data'; |
||||
import { CustomScrollbar, Icon, IconName, LoadingPlaceholder, useStyles, useStyles2 } from '@grafana/ui'; |
||||
import { css } from '@emotion/css'; |
||||
|
||||
import { AlertInstances } from './AlertInstances'; |
||||
import alertDef from 'app/features/alerting/state/alertDef'; |
||||
import { SortOrder, UnifiedAlertListOptions } from './types'; |
||||
|
||||
import { flattenRules, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; |
||||
import { PromRuleWithLocation } from 'app/types/unified-alerting'; |
||||
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions'; |
||||
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector'; |
||||
import { getAllRulesSourceNames } from 'app/features/alerting/unified/utils/datasource'; |
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; |
||||
import { Annotation, RULE_LIST_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; |
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; |
||||
|
||||
export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) { |
||||
const dispatch = useDispatch(); |
||||
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); |
||||
|
||||
useEffect(() => { |
||||
dispatch(fetchAllPromRulesAction()); |
||||
const interval = setInterval(() => dispatch(fetchAllPromRulesAction()), RULE_LIST_POLL_INTERVAL_MS); |
||||
return () => { |
||||
clearInterval(interval); |
||||
}; |
||||
}, [dispatch]); |
||||
|
||||
const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules); |
||||
|
||||
const dispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched); |
||||
const loading = rulesDataSourceNames.some((name) => promRulesRequests[name]?.loading); |
||||
const haveResults = rulesDataSourceNames.some( |
||||
(name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error |
||||
); |
||||
|
||||
const styles = useStyles(getStyles); |
||||
const stateStyle = useStyles2(getStateTagStyles); |
||||
|
||||
const rules = useMemo( |
||||
() => |
||||
filterRules( |
||||
props.options, |
||||
sortRules( |
||||
props.options.sortOrder, |
||||
Object.values(promRulesRequests).flatMap(({ result = [] }) => flattenRules(result)) |
||||
) |
||||
), |
||||
[props.options, promRulesRequests] |
||||
); |
||||
|
||||
const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems); |
||||
|
||||
const noAlertsMessage = rules.length ? '' : 'No alerts'; |
||||
|
||||
return ( |
||||
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%"> |
||||
<div className={styles.container}> |
||||
{dispatched && loading && !haveResults && <LoadingPlaceholder text="Loading..." />} |
||||
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>} |
||||
<section> |
||||
<ol className={styles.alertRuleList}> |
||||
{haveResults && |
||||
rulesToDisplay.map((ruleWithLocation, index) => { |
||||
const { rule, namespaceName, groupName } = ruleWithLocation; |
||||
const firstActiveAt = getFirstActiveAt(rule); |
||||
return ( |
||||
<li |
||||
className={styles.alertRuleItem} |
||||
key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`} |
||||
> |
||||
<div className={stateStyle.icon}> |
||||
<Icon |
||||
name={alertDef.getStateDisplayModel(rule.state).iconClass as IconName} |
||||
className={stateStyle[alertStateToState[rule.state]]} |
||||
size={'lg'} |
||||
/> |
||||
</div> |
||||
<div> |
||||
<div className={styles.instanceDetails}> |
||||
<div className={styles.alertName} title={rule.name}> |
||||
{rule.name} |
||||
</div> |
||||
<div className={styles.alertDuration}> |
||||
<span className={stateStyle[alertStateToState[rule.state]]}>{rule.state.toUpperCase()}</span>{' '} |
||||
{firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && ( |
||||
<> |
||||
for{' '} |
||||
<span> |
||||
{intervalToAbbreviatedDurationString({ |
||||
start: firstActiveAt, |
||||
end: Date.now(), |
||||
})} |
||||
</span> |
||||
</> |
||||
)} |
||||
</div> |
||||
</div> |
||||
<AlertInstances ruleWithLocation={ruleWithLocation} showInstances={props.options.showInstances} /> |
||||
</div> |
||||
</li> |
||||
); |
||||
})} |
||||
</ol> |
||||
</section> |
||||
</div> |
||||
</CustomScrollbar> |
||||
); |
||||
} |
||||
|
||||
function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) { |
||||
if (sortOrder === SortOrder.Importance) { |
||||
// @ts-ignore
|
||||
return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]); |
||||
} else if (sortOrder === SortOrder.TimeAsc) { |
||||
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date()); |
||||
} else if (sortOrder === SortOrder.TimeDesc) { |
||||
return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date()).reverse(); |
||||
} |
||||
const result = sortBy(rules, (rule) => rule.rule.name.toLowerCase()); |
||||
if (sortOrder === SortOrder.AlphaDesc) { |
||||
result.reverse(); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
function filterRules(options: PanelProps<UnifiedAlertListOptions>['options'], rules: PromRuleWithLocation[]) { |
||||
let filteredRules = [...rules]; |
||||
if (options.dashboardAlerts) { |
||||
const dashboardUid = getDashboardSrv().getCurrent()?.uid; |
||||
filteredRules = filteredRules.filter(({ rule: { annotations = {} } }) => |
||||
Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid) |
||||
); |
||||
} |
||||
if (options.alertName) { |
||||
filteredRules = filteredRules.filter(({ rule: { name } }) => |
||||
name.toLocaleLowerCase().includes(options.alertName.toLocaleLowerCase()) |
||||
); |
||||
} |
||||
if (Object.values(options.stateFilter).some((value) => value)) { |
||||
filteredRules = filteredRules.filter((rule) => { |
||||
return ( |
||||
(options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) || |
||||
(options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) || |
||||
(options.stateFilter.inactive && rule.rule.state === PromAlertingRuleState.Inactive) |
||||
); |
||||
}); |
||||
} |
||||
if (options.folder) { |
||||
filteredRules = filteredRules.filter((rule) => { |
||||
return rule.namespaceName === options.folder.title; |
||||
}); |
||||
} |
||||
|
||||
return filteredRules; |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({ |
||||
cardContainer: css` |
||||
padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0; |
||||
line-height: ${theme.typography.lineHeight.md}; |
||||
margin-bottom: 0px; |
||||
`,
|
||||
container: css` |
||||
overflow-y: auto; |
||||
height: 100%; |
||||
`,
|
||||
alertRuleList: css` |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
justify-content: space-between; |
||||
list-style-type: none; |
||||
`,
|
||||
alertRuleItem: css` |
||||
display: flex; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
background: ${theme.colors.bg2}; |
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm}; |
||||
border-radius: ${theme.border.radius.md}; |
||||
margin-bottom: ${theme.spacing.xs}; |
||||
|
||||
& > * { |
||||
margin-right: ${theme.spacing.sm}; |
||||
} |
||||
`,
|
||||
alertName: css` |
||||
font-size: ${theme.typography.size.md}; |
||||
font-weight: ${theme.typography.weight.bold}; |
||||
`,
|
||||
alertDuration: css` |
||||
font-size: ${theme.typography.size.sm}; |
||||
`,
|
||||
alertRuleItemText: css` |
||||
font-weight: ${theme.typography.weight.bold}; |
||||
font-size: ${theme.typography.size.sm}; |
||||
margin: 0; |
||||
`,
|
||||
alertRuleItemTime: css` |
||||
color: ${theme.colors.textWeak}; |
||||
font-weight: normal; |
||||
white-space: nowrap; |
||||
`,
|
||||
alertRuleItemInfo: css` |
||||
font-weight: normal; |
||||
flex-grow: 2; |
||||
display: flex; |
||||
align-items: flex-end; |
||||
`,
|
||||
noAlertsMessage: css` |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
`,
|
||||
alertIcon: css` |
||||
margin-right: ${theme.spacing.xs}; |
||||
`,
|
||||
instanceDetails: css` |
||||
min-width: 1px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
`,
|
||||
}); |
||||
|
||||
const getStateTagStyles = (theme: GrafanaTheme2) => ({ |
||||
common: css` |
||||
width: 70px; |
||||
text-align: center; |
||||
align-self: stretch; |
||||
|
||||
display: inline-block; |
||||
color: white; |
||||
border-radius: ${theme.shape.borderRadius()}; |
||||
font-size: ${theme.typography.size.sm}; |
||||
/* padding: ${theme.spacing(2, 0)}; */ |
||||
text-transform: capitalize; |
||||
line-height: 1.2; |
||||
flex-shrink: 0; |
||||
|
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
`,
|
||||
icon: css` |
||||
margin-top: ${theme.spacing(2.5)}; |
||||
align-self: flex-start; |
||||
`,
|
||||
// good: css`
|
||||
// background-color: ${theme.colors.success.main};
|
||||
// border: solid 1px ${theme.colors.success.main};
|
||||
// color: ${theme.colors.success.contrastText};
|
||||
// `,
|
||||
// warning: css`
|
||||
// background-color: ${theme.colors.warning.main};
|
||||
// border: solid 1px ${theme.colors.warning.main};
|
||||
// color: ${theme.colors.warning.contrastText};
|
||||
// `,
|
||||
// bad: css`
|
||||
// background-color: ${theme.colors.error.main};
|
||||
// border: solid 1px ${theme.colors.error.main};
|
||||
// color: ${theme.colors.error.contrastText};
|
||||
// `,
|
||||
// neutral: css`
|
||||
// background-color: ${theme.colors.secondary.main};
|
||||
// border: solid 1px ${theme.colors.secondary.main};
|
||||
// `,
|
||||
// info: css`
|
||||
// background-color: ${theme.colors.primary.main};
|
||||
// border: solid 1px ${theme.colors.primary.main};
|
||||
// color: ${theme.colors.primary.contrastText};
|
||||
// `,
|
||||
good: css` |
||||
color: ${theme.colors.success.main}; |
||||
`,
|
||||
bad: css` |
||||
color: ${theme.colors.error.main}; |
||||
`,
|
||||
warning: css` |
||||
color: ${theme.colors.warning.main}; |
||||
`,
|
||||
neutral: css` |
||||
color: ${theme.colors.secondary.main}; |
||||
`,
|
||||
info: css` |
||||
color: ${theme.colors.primary.main}; |
||||
`,
|
||||
}); |
Loading…
Reference in new issue