diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index 9beee9941c1..5ff067820a2 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -91,7 +91,7 @@ export const DynamicTable = ({ {items.map((item, index) => { const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); return ( -
+
{renderPrefixCell && renderPrefixCell(item, index, items)} {isExpandable && (
diff --git a/public/app/features/alerting/unified/utils/redux.ts b/public/app/features/alerting/unified/utils/redux.ts index 4bc6cb1819f..345164d3c9c 100644 --- a/public/app/features/alerting/unified/utils/redux.ts +++ b/public/app/features/alerting/unified/utils/redux.ts @@ -161,3 +161,19 @@ export function messageFromError(e: Error | FetchError | SerializedError): strin } return (e as Error)?.message || String(e); } + +export function isAsyncRequestMapSliceFulfilled(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).every(isAsyncRequestStateFulfilled); +} + +export function isAsyncRequestStateFulfilled(state: AsyncRequestState): boolean { + return state.dispatched && !state.loading && !state.error; +} + +export function isAsyncRequestMapSlicePending(slice: AsyncRequestMapSlice): boolean { + return Object.values(slice).some(isAsyncRequestStatePending); +} + +export function isAsyncRequestStatePending(state: AsyncRequestState): boolean { + return state.dispatched && state.loading; +} diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx index 1b5a049b1d7..269a3ca0a92 100644 --- a/public/app/plugins/panel/alertlist/AlertInstances.tsx +++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx @@ -1,67 +1,47 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import pluralize from 'pluralize'; import { Icon, useStyles2 } from '@grafana/ui'; -import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting'; +import { Alert } from 'app/types/unified-alerting'; import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { css } from '@emotion/css'; -import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; -import { UnifiedAlertListOptions } from './types'; +import { GroupMode, UnifiedAlertListOptions } from './types'; import { AlertInstancesTable } from 'app/features/alerting/unified/components/rules/AlertInstancesTable'; import { sortAlerts } from 'app/features/alerting/unified/utils/misc'; +import { filterAlerts } from './util'; interface Props { - ruleWithLocation: PromRuleWithLocation; + alerts: Alert[]; options: PanelProps['options']; } -export const AlertInstances = ({ ruleWithLocation, options }: Props) => { - const { rule } = ruleWithLocation; - const [displayInstances, setDisplayInstances] = useState(options.showInstances); +export const AlertInstances: FC = ({ alerts, options }) => { + // when custom grouping is enabled, we will always uncollapse the list of alert instances + const defaultShowInstances = options.groupMode === GroupMode.Custom ? true : options.showInstances; + const [displayInstances, setDisplayInstances] = useState(defaultShowInstances); const styles = useStyles2(getStyles); - useEffect(() => { - setDisplayInstances(options.showInstances); - }, [options.showInstances]); + const toggleDisplayInstances = useCallback(() => { + setDisplayInstances((display) => !display); + }, []); - const alerts = useMemo( - (): Alert[] => (displayInstances ? filterAlerts(options, sortAlerts(options.sortOrder, rule.alerts)) : []), - [rule, options, displayInstances] + const filteredAlerts = useMemo( + (): Alert[] => filterAlerts(options, sortAlerts(options.sortOrder, alerts)) ?? [], + [alerts, options] ); return (
- {rule.state !== PromAlertingRuleState.Inactive && ( -
setDisplayInstances(!displayInstances)}> + {options.groupMode === GroupMode.Default && ( +
toggleDisplayInstances()}> - {`${rule.alerts.length} ${pluralize('instance', rule.alerts.length)}`} + {`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`}
)} - - {!!alerts.length && } + {displayInstances && }
); }; -function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { - const hasAlertState = Object.values(options.stateFilter).some((value) => value); - let filteredAlerts = [...alerts]; - if (hasAlertState) { - filteredAlerts = filteredAlerts.filter((alert) => { - return ( - (options.stateFilter.firing && - (alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) || - (options.stateFilter.pending && - (alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) || - (options.stateFilter.noData && alert.state === GrafanaAlertState.NoData) || - (options.stateFilter.normal && alert.state === GrafanaAlertState.Normal) || - (options.stateFilter.error && alert.state === GrafanaAlertState.Error) || - (options.stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive) - ); - }); - } - return filteredAlerts; -} - const getStyles = (_: GrafanaTheme2) => ({ instance: css` cursor: pointer; diff --git a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx new file mode 100644 index 00000000000..8a84dcaa55d --- /dev/null +++ b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx @@ -0,0 +1,75 @@ +import React, { FC, useEffect, useMemo } from 'react'; +import { isEmpty, uniq } from 'lodash'; +import { Icon, MultiSelect } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; +import { useDispatch } from 'react-redux'; +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 { PromRuleType } from 'app/types/unified-alerting-dto'; +import { AlertingRule } from 'app/types/unified-alerting'; +import { isPrivateLabel } from './util'; +import { + isAsyncRequestMapSliceFulfilled, + isAsyncRequestMapSlicePending, +} from 'app/features/alerting/unified/utils/redux'; + +interface Props { + id: string; + defaultValue: SelectableValue; + onChange: (keys: string[]) => void; +} + +export const GroupBy: FC = (props) => { + const { onChange, id, defaultValue } = props; + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchAllPromRulesAction()); + }, [dispatch]); + + const promRulesByDatasource = useUnifiedAlertingSelector((state) => state.promRules); + const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); + + const allRequestsReady = isAsyncRequestMapSliceFulfilled(promRulesByDatasource); + const loading = isAsyncRequestMapSlicePending(promRulesByDatasource); + + const labels = useMemo(() => { + if (isEmpty(promRulesByDatasource)) { + return []; + } + + if (!allRequestsReady) { + return []; + } + + const allLabels = rulesDataSourceNames + .flatMap((datasource) => promRulesByDatasource[datasource].result ?? []) + .flatMap((rules) => rules.groups) + .flatMap((group) => group.rules.filter((rule): rule is AlertingRule => rule.type === PromRuleType.Alerting)) + .flatMap((rule) => rule.alerts ?? []) + .map((alert) => Object.keys(alert.labels ?? {})) + .flatMap((labels) => labels.filter(isPrivateLabel)); + + return uniq(allLabels); + }, [allRequestsReady, promRulesByDatasource, rulesDataSourceNames]); + + return ( + + id={id} + isLoading={loading} + defaultValue={defaultValue} + aria-label={'group by label keys'} + placeholder="Group by" + prefix={} + onChange={(items) => { + onChange(items.map((item) => item.value ?? '')); + }} + options={labels.map((key) => ({ + label: key, + value: key, + }))} + menuShouldPortal={true} + /> + ); +}; diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index e532878c896..6d8488d8629 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -1,15 +1,14 @@ 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 { GrafanaTheme2, PanelProps } from '@grafana/data'; +import { CustomScrollbar, LoadingPlaceholder, 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 { GroupMode, SortOrder, UnifiedAlertListOptions } from './types'; -import { flattenRules, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; +import { flattenRules, 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'; @@ -22,6 +21,8 @@ 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'; import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; +import UngroupedModeView from './unified-alerting/UngroupedView'; +import GroupedModeView from './unified-alerting/GroupedView'; export function UnifiedAlertList(props: PanelProps) { const dispatch = useDispatch(); @@ -43,8 +44,7 @@ export function UnifiedAlertList(props: PanelProps) { (name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error ); - const styles = useStyles(getStyles); - const stateStyle = useStyles2(getStateTagStyles); + const styles = useStyles2(getStyles); const rules = useMemo( () => @@ -58,8 +58,6 @@ export function UnifiedAlertList(props: PanelProps) { [props.options, promRulesRequests] ); - const rulesToDisplay = rules.length <= props.options.maxItems ? rules : rules.slice(0, props.options.maxItems); - const noAlertsMessage = rules.length ? '' : 'No alerts'; return ( @@ -68,49 +66,12 @@ export function UnifiedAlertList(props: PanelProps) { {dispatched && loading && !haveResults && } {noAlertsMessage &&
{noAlertsMessage}
}
-
    - {haveResults && - rulesToDisplay.map((ruleWithLocation, index) => { - const { rule, namespaceName, groupName } = ruleWithLocation; - const firstActiveAt = getFirstActiveAt(rule); - return ( -
  1. -
    - -
    -
    -
    -
    - {rule.name} -
    -
    - {rule.state.toUpperCase()}{' '} - {firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && ( - <> - for{' '} - - {intervalToAbbreviatedDurationString({ - start: firstActiveAt, - end: Date.now(), - })} - - - )} -
    -
    - -
    -
  2. - ); - })} -
+ {props.options.groupMode === GroupMode.Custom && haveResults && ( + + )} + {props.options.groupMode === GroupMode.Default && haveResults && ( + + )}
@@ -160,7 +121,7 @@ function filterRules(options: PanelProps['options'], ru const matchers = parseMatchers(options.alertInstanceLabelFilter); // Reduce rules and instances to only those that match filteredRules = filteredRules.reduce((rules, rule) => { - const filteredAlerts = rule.rule.alerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers)); + const filteredAlerts = (rule.rule.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers)); if (filteredAlerts.length) { rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } }); } @@ -185,10 +146,10 @@ function filterRules(options: PanelProps['options'], ru return filteredRules; } -const getStyles = (theme: GrafanaTheme) => ({ +export const getStyles = (theme: GrafanaTheme2) => ({ cardContainer: css` - padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0; - line-height: ${theme.typography.lineHeight.md}; + padding: ${theme.v1.spacing.xs} 0 ${theme.v1.spacing.xxs} 0; + line-height: ${theme.v1.typography.lineHeight.md}; margin-bottom: 0px; `, container: css` @@ -206,29 +167,34 @@ const getStyles = (theme: GrafanaTheme) => ({ 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}; + background: ${theme.v1.colors.bg2}; + padding: ${theme.v1.spacing.xs} ${theme.v1.spacing.sm}; + border-radius: ${theme.v1.border.radius.md}; + margin-bottom: ${theme.v1.spacing.xs}; & > * { - margin-right: ${theme.spacing.sm}; + margin-right: ${theme.v1.spacing.sm}; } `, alertName: css` - font-size: ${theme.typography.size.md}; - font-weight: ${theme.typography.weight.bold}; + font-size: ${theme.v1.typography.size.md}; + font-weight: ${theme.v1.typography.weight.bold}; + `, + alertLabels: css` + > * { + margin-right: ${theme.v1.spacing.xs}; + } `, alertDuration: css` - font-size: ${theme.typography.size.sm}; + font-size: ${theme.v1.typography.size.sm}; `, alertRuleItemText: css` - font-weight: ${theme.typography.weight.bold}; - font-size: ${theme.typography.size.sm}; + font-weight: ${theme.v1.typography.weight.bold}; + font-size: ${theme.v1.typography.size.sm}; margin: 0; `, alertRuleItemTime: css` - color: ${theme.colors.textWeak}; + color: ${theme.v1.colors.textWeak}; font-weight: normal; white-space: nowrap; `, @@ -246,7 +212,7 @@ const getStyles = (theme: GrafanaTheme) => ({ height: 100%; `, alertIcon: css` - margin-right: ${theme.spacing.xs}; + margin-right: ${theme.v1.spacing.xs}; `, instanceDetails: css` min-width: 1px; @@ -254,68 +220,7 @@ const getStyles = (theme: GrafanaTheme) => ({ 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}; + customGroupDetails: css` + margin-bottom: ${theme.v1.spacing.xs}; `, }); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 823495fd48e..30e504971ea 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -3,7 +3,7 @@ import { PanelPlugin } from '@grafana/data'; import { TagsInput } from '@grafana/ui'; import { AlertList } from './AlertList'; import { UnifiedAlertList } from './UnifiedAlertList'; -import { AlertListOptions, ShowOption, SortOrder, UnifiedAlertListOptions } from './types'; +import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions } from './types'; import { alertListPanelMigrationHandler } from './AlertListMigrationHandler'; import { config, DataSourcePicker } from '@grafana/runtime'; import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker'; @@ -13,6 +13,7 @@ import { ReadonlyFolderPicker, } from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker'; import { AlertListSuggestionsSupplier } from './suggestions'; +import { GroupBy } from './GroupByWithLoading'; function showIfCurrentState(options: AlertListOptions) { return options.showOptions === ShowOption.Current; @@ -151,6 +152,37 @@ const alertList = new PanelPlugin(AlertList) const unifiedAlertList = new PanelPlugin(UnifiedAlertList).setPanelOptions((builder) => { builder + .addRadio({ + path: 'groupMode', + name: 'Group mode', + description: 'How alert instances should be grouped', + defaultValue: GroupMode.Default, + settings: { + options: [ + { value: GroupMode.Default, label: 'Default grouping' }, + { value: GroupMode.Custom, label: 'Custom grouping' }, + ], + }, + category: ['Options'], + }) + .addCustomEditor({ + path: 'groupBy', + name: 'Group by', + description: 'Filter alerts using label querying', + id: 'groupBy', + defaultValue: [], + showIf: (options) => options.groupMode === GroupMode.Custom, + category: ['Options'], + editor: (props) => { + return ( + ({ label: value, value }))} + onChange={props.onChange} + /> + ); + }, + }) .addNumberInput({ name: 'Max items', path: 'maxItems', @@ -181,13 +213,6 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi defaultValue: false, category: ['Options'], }) - .addBooleanSwitch({ - path: 'showInstances', - name: 'Show alert instances', - description: 'Show individual alert instances for multi-dimensional rules', - defaultValue: false, - category: ['Options'], - }) .addTextInput({ path: 'alertName', name: 'Alert name', diff --git a/public/app/plugins/panel/alertlist/types.ts b/public/app/plugins/panel/alertlist/types.ts index 4b2d313bcb3..76983e8922a 100644 --- a/public/app/plugins/panel/alertlist/types.ts +++ b/public/app/plugins/panel/alertlist/types.ts @@ -1,3 +1,5 @@ +import { Alert } from 'app/types/unified-alerting'; + export enum SortOrder { AlphaAsc = 1, AlphaDesc, @@ -11,6 +13,11 @@ export enum ShowOption { RecentChanges = 'changes', } +export enum GroupMode { + Default = 'default', + Custom = 'custom', +} + export interface AlertListOptions { showOptions: ShowOption; maxItems: number; @@ -43,6 +50,8 @@ export interface UnifiedAlertListOptions { maxItems: number; sortOrder: SortOrder; dashboardAlerts: boolean; + groupMode: GroupMode; + groupBy: string[]; alertName: string; showInstances: boolean; folder: { id: number; title: string }; @@ -50,3 +59,5 @@ export interface UnifiedAlertListOptions { alertInstanceLabelFilter: string; datasource: string; } + +export type GroupedRules = Map; diff --git a/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx new file mode 100644 index 00000000000..fc2995361e1 --- /dev/null +++ b/public/app/plugins/panel/alertlist/unified-alerting/GroupedView.tsx @@ -0,0 +1,70 @@ +import React, { FC, useMemo } from 'react'; +import { useStyles2 } from '@grafana/ui'; +import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel'; +import { AlertInstances } from '../AlertInstances'; +import { GroupedRules, UnifiedAlertListOptions } from '../types'; +import { getStyles } from '../UnifiedAlertList'; +import { PromRuleWithLocation } from 'app/types/unified-alerting'; + +type GroupedModeProps = { + rules: PromRuleWithLocation[]; + options: UnifiedAlertListOptions; +}; + +const GroupedModeView: FC = ({ rules, options }) => { + const styles = useStyles2(getStyles); + + const groupBy = options.groupBy; + + const groupedRules = useMemo(() => { + const groupedRules = new Map(); + + const hasInstancesWithMatchingLabels = (rule: PromRuleWithLocation) => + groupBy ? alertHasEveryLabel(rule, groupBy) : true; + + const matchingRules = rules.filter(hasInstancesWithMatchingLabels); + matchingRules.forEach((rule: PromRuleWithLocation) => { + (rule.rule.alerts ?? []).forEach((alert) => { + const mapKey = createMapKey(groupBy, alert.labels); + const existingAlerts = groupedRules.get(mapKey) ?? []; + groupedRules.set(mapKey, [...existingAlerts, alert]); + }); + }); + + return groupedRules; + }, [groupBy, rules]); + + return ( + <> + {Array.from(groupedRules).map(([key, alerts]) => ( +
  • +
    +
    +
    + {key && parseMapKey(key).map(([key, value]) => )} + {!key && 'No grouping'} +
    +
    + +
    +
  • + ))} + + ); +}; + +function createMapKey(groupBy: string[], labels: Record): string { + return new URLSearchParams(groupBy.map((key) => [key, labels[key]])).toString(); +} + +function parseMapKey(key: string): Array<[string, string]> { + return [...new URLSearchParams(key)]; +} + +function alertHasEveryLabel(rule: PromRuleWithLocation, groupByKeys: string[]) { + return groupByKeys.every((key) => { + return (rule.rule.alerts ?? []).some((alert) => alert.labels[key]); + }); +} + +export default GroupedModeView; diff --git a/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx new file mode 100644 index 00000000000..8588ec7062f --- /dev/null +++ b/public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx @@ -0,0 +1,108 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; +import { Icon, IconName, useStyles2 } from '@grafana/ui'; +import alertDef from 'app/features/alerting/state/alertDef'; +import { alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules'; +import { PromRuleWithLocation } from 'app/types/unified-alerting'; +import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import React, { FC } from 'react'; +import { AlertInstances } from '../AlertInstances'; +import { UnifiedAlertListOptions } from '../types'; +import { getStyles } from '../UnifiedAlertList'; + +type UngroupedModeProps = { + rules: PromRuleWithLocation[]; + options: UnifiedAlertListOptions; +}; + +const UngroupedModeView: FC = ({ rules, options }) => { + const styles = useStyles2(getStyles); + const stateStyle = useStyles2(getStateTagStyles); + + const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems); + + return ( + <> +
      + {rulesToDisplay.map((ruleWithLocation, index) => { + const { rule, namespaceName, groupName } = ruleWithLocation; + const firstActiveAt = getFirstActiveAt(rule); + return ( +
    1. +
      + +
      +
      +
      +
      + {rule.name} +
      +
      + {rule.state.toUpperCase()}{' '} + {firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && ( + <> + for{' '} + + {intervalToAbbreviatedDurationString({ + start: firstActiveAt, + end: Date.now(), + })} + + + )} +
      +
      + +
      +
    2. + ); + })} +
    + + ); +}; + +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.v1.typography.size.sm}; + 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` + 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}; + `, +}); + +export default UngroupedModeView; diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts new file mode 100644 index 00000000000..7d00f2237a3 --- /dev/null +++ b/public/app/plugins/panel/alertlist/util.ts @@ -0,0 +1,30 @@ +import { PanelProps } from '@grafana/data'; +import { Alert } from 'app/types/unified-alerting'; +import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import { isEmpty } from 'lodash'; +import { UnifiedAlertListOptions } from './types'; + +export function filterAlerts(options: PanelProps['options'], alerts: Alert[]): Alert[] { + const { stateFilter } = options; + + if (isEmpty(stateFilter)) { + return alerts; + } + + return alerts.filter((alert) => { + return ( + (stateFilter.firing && + (alert.state === GrafanaAlertState.Alerting || alert.state === PromAlertingRuleState.Firing)) || + (stateFilter.pending && + (alert.state === GrafanaAlertState.Pending || alert.state === PromAlertingRuleState.Pending)) || + (stateFilter.noData && alert.state === GrafanaAlertState.NoData) || + (stateFilter.normal && alert.state === GrafanaAlertState.Normal) || + (stateFilter.error && alert.state === GrafanaAlertState.Error) || + (stateFilter.inactive && alert.state === PromAlertingRuleState.Inactive) + ); + }); +} + +export function isPrivateLabel(label: string) { + return !(label.startsWith('__') && label.endsWith('__')); +} diff --git a/public/app/types/unified-alerting.ts b/public/app/types/unified-alerting.ts index 2ee1d9213ed..50c4c5f4c51 100644 --- a/public/app/types/unified-alerting.ts +++ b/public/app/types/unified-alerting.ts @@ -28,7 +28,7 @@ interface RuleBase { } export interface AlertingRule extends RuleBase { - alerts: Alert[]; + alerts?: Alert[]; labels: { [key: string]: string; };