Merge branch 'alerting/fix-cash-missing-transitions' into alerting/cash-improvements

alerting/cash-improvements
Gilles De Mey 10 months ago
commit 461e7f636f
No known key found for this signature in database
  1. 161
      public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
  2. 6
      public/app/features/alerting/unified/components/rules/central-state-history/utils.ts

@ -1,8 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { ReactElement, useMemo, useState } from 'react'; import { ReactElement, useMemo } from 'react';
import { useLocation } from 'react-router'; import { useMeasure, useToggle } from 'react-use';
import { useMeasure } from 'react-use';
import { DataFrameJSON, GrafanaTheme2, IconName, TimeRange } from '@grafana/data'; import { DataFrameJSON, GrafanaTheme2, IconName, TimeRange } from '@grafana/data';
import { import {
@ -15,14 +14,16 @@ import {
} from '@grafana/scenes'; } from '@grafana/scenes';
import { import {
Alert, Alert,
Button,
EmptyState, EmptyState,
Icon, Icon,
LoadingBar, LoadingBar,
Pagination, Pagination,
Stack, Stack,
Text, Text,
Tooltip, TextLink,
useStyles2, useStyles2,
useTheme2,
withErrorBoundary, withErrorBoundary,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
@ -42,7 +43,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers'; import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { createRelativeUrl } from '../../../utils/url'; import { createRelativeUrl } from '../../../utils/url';
import { AlertLabels } from '../../AlertLabels'; import { AlertLabels } from '../../AlertLabels';
import { CollapseToggle } from '../../CollapseToggle'; import { WithReturnButton } from '../../WithReturnButton';
import { LogRecord } from '../state-history/common'; import { LogRecord } from '../state-history/common';
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords'; import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
@ -201,7 +202,8 @@ interface EventRowProps {
} }
function EventRow({ record, addFilter, timeRange }: EventRowProps) { function EventRow({ record, addFilter, timeRange }: EventRowProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, toggleCollapsed] = useToggle(true);
function onLabelClick(label: string, value: string) { function onLabelClick(label: string, value: string) {
addFilter(label, value, 'label'); addFilter(label, value, 'label');
} }
@ -211,13 +213,8 @@ function EventRow({ record, addFilter, timeRange }: EventRowProps) {
<div <div
className={cx(styles.header, isCollapsed ? styles.collapsedHeader : styles.notCollapsedHeader)} className={cx(styles.header, isCollapsed ? styles.collapsedHeader : styles.notCollapsedHeader)}
data-testid="event-row-header" data-testid="event-row-header"
onClick={() => toggleCollapsed()}
> >
<CollapseToggle
size="sm"
className={styles.collapseToggle}
isCollapsed={isCollapsed}
onToggle={setIsCollapsed}
/>
<Stack gap={0.5} direction={'row'} alignItems={'center'}> <Stack gap={0.5} direction={'row'} alignItems={'center'}>
<div className={styles.timeCol}> <div className={styles.timeCol}>
<Timestamp time={record.timestamp} /> <Timestamp time={record.timestamp} />
@ -247,10 +244,8 @@ interface AlertRuleNameProps {
ruleUID?: string; ruleUID?: string;
} }
function AlertRuleName({ labels, ruleUID }: AlertRuleNameProps) { function AlertRuleName({ labels, ruleUID }: AlertRuleNameProps) {
const styles = useStyles2(getStyles);
const { pathname, search } = useLocation();
const returnTo = `${pathname}${search}`;
const alertRuleName = labels.alertname; const alertRuleName = labels.alertname;
if (!ruleUID) { if (!ruleUID) {
return ( return (
<Text> <Text>
@ -258,16 +253,20 @@ function AlertRuleName({ labels, ruleUID }: AlertRuleNameProps) {
</Text> </Text>
); );
} }
const ruleViewUrl = createRelativeUrl(`/alerting/${GRAFANA_RULES_SOURCE_NAME}/${ruleUID}/view`, { const ruleViewUrl = createRelativeUrl(`/alerting/${GRAFANA_RULES_SOURCE_NAME}/${ruleUID}/view`, {
tab: 'history', tab: 'history',
returnTo,
}); });
return ( return (
<Tooltip content={alertRuleName ?? ''}> <WithReturnButton
<a href={ruleViewUrl} className={styles.alertName}> title="History"
{alertRuleName} component={
</a> <TextLink href={ruleViewUrl} inline={false} title={alertRuleName}>
</Tooltip> {alertRuleName}
</TextLink>
}
/>
); );
} }
@ -278,7 +277,7 @@ interface EventTransitionProps {
} }
function EventTransition({ previous, current, addFilter }: EventTransitionProps) { function EventTransition({ previous, current, addFilter }: EventTransitionProps) {
return ( return (
<Stack gap={0.5} direction={'row'}> <Stack gap={0} direction="row" alignItems="center">
<EventState state={previous} addFilter={addFilter} type="from" /> <EventState state={previous} addFilter={addFilter} type="from" />
<Icon name="arrow-right" size="lg" /> <Icon name="arrow-right" size="lg" />
<EventState state={current} addFilter={addFilter} type="to" /> <EventState state={current} addFilter={addFilter} type="to" />
@ -289,21 +288,18 @@ function EventTransition({ previous, current, addFilter }: EventTransitionProps)
interface StateIconProps { interface StateIconProps {
iconName: IconName; iconName: IconName;
iconColor: string; iconColor: string;
tooltipContent: string;
labelText: ReactElement; labelText: ReactElement;
showLabel: boolean; showLabel: boolean;
} }
const StateIcon = ({ iconName, iconColor, tooltipContent, labelText, showLabel }: StateIconProps) => ( const StateIcon = ({ iconName, iconColor, labelText, showLabel }: StateIconProps) => (
<Tooltip content={tooltipContent} placement="top"> <Stack gap={0.5} direction="row" alignItems="center">
<Stack gap={0.5} direction={'row'} alignItems="center"> <Icon name={iconName} size="md" color={iconColor} />
<Icon name={iconName} size="md" className={iconColor} /> {showLabel && (
{showLabel && ( <Text variant="body" weight="light">
<Text variant="body" weight="light"> {labelText}
{labelText} </Text>
</Text> )}
)} </Stack>
</Stack>
</Tooltip>
); );
interface StateConfig { interface StateConfig {
@ -324,75 +320,74 @@ interface EventStateProps {
type: 'from' | 'to'; type: 'from' | 'to';
} }
export function EventState({ state, showLabel = false, addFilter, type }: EventStateProps) { export function EventState({ state, showLabel = false, addFilter, type }: EventStateProps) {
const styles = useStyles2(getStyles); const theme = useTheme2();
const toolTip = t('alerting.central-alert-history.details.no-recognized-state', 'No recognized state');
const unknownState: StateConfig = {
iconName: 'exclamation-triangle',
iconColor: theme.colors.secondary.text,
tooltipContent: t('alerting.central-alert-history.details.no-recognized-state', 'Unknown state'),
labelText: <Trans i18nKey="alerting.central-alert-history.details.no-recognized-state">Unknown state</Trans>,
};
if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) { if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) {
return ( <StateIcon {...unknownState} showLabel={showLabel} />;
<StateIcon
iconName="exclamation-triangle"
tooltipContent={toolTip}
labelText={<Trans i18nKey="alerting.central-alert-history.details.unknown-event-state">Unknown</Trans>}
showLabel={showLabel}
iconColor={styles.warningColor}
/>
);
} }
const baseState = mapStateWithReasonToBaseState(state); const baseState = mapStateWithReasonToBaseState(state);
const reason = mapStateWithReasonToReason(state); const reason = mapStateWithReasonToReason(state);
const stateConfig: StateConfigMap = { const stateConfig: StateConfigMap = {
Normal: { Normal: {
iconName: 'check-circle', iconName: 'check-circle',
iconColor: Boolean(reason) ? styles.warningColor : styles.normalColor, iconColor: theme.colors.success.text,
tooltipContent: Boolean(reason) ? `Normal (${reason})` : 'Normal', tooltipContent: Boolean(reason) ? `Normal (${reason})` : 'Normal',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.normal">Normal</Trans>, labelText: <Trans i18nKey="alerting.central-alert-history.details.state.normal">Normal</Trans>,
}, },
Alerting: { Alerting: {
iconName: 'exclamation-circle', iconName: 'exclamation-circle',
iconColor: styles.alertingColor, iconColor: theme.colors.error.text,
tooltipContent: 'Alerting', tooltipContent: 'Alerting',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.alerting">Alerting</Trans>, labelText: <Trans i18nKey="alerting.central-alert-history.details.state.alerting">Alerting</Trans>,
}, },
NoData: { NoData: {
iconName: 'exclamation-triangle', iconName: 'exclamation-triangle',
iconColor: styles.warningColor, iconColor: theme.colors.warning.text,
tooltipContent: 'Insufficient data', tooltipContent: 'Insufficient data',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.no-data">No data</Trans>, labelText: <Trans i18nKey="alerting.central-alert-history.details.state.no-data">No data</Trans>,
}, },
Error: { Error: {
iconName: 'exclamation-circle', iconName: 'times-circle',
tooltipContent: 'Error', tooltipContent: 'Error',
iconColor: styles.warningColor, iconColor: theme.colors.error.text,
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.error">Error</Trans>, labelText: <Trans i18nKey="alerting.central-alert-history.details.state.error">Error</Trans>,
}, },
Pending: { Pending: {
iconName: 'circle', iconName: 'circle',
iconColor: styles.warningColor, iconColor: theme.colors.warning.text,
tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending', tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.pending">Pending</Trans>, labelText: <Trans i18nKey="alerting.central-alert-history.details.state.pending">Pending</Trans>,
}, },
Paused: {
iconName: 'pause-circle',
iconColor: theme.colors.warning.text,
tooltipContent: Boolean(reason) ? `${baseState} (${reason})` : baseState,
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.paused">Paused</Trans>,
},
}; };
function onStateClick() { function onStateClick() {
addFilter('state', baseState, type === 'from' ? 'stateFrom' : 'stateTo'); addFilter('state', baseState, type === 'from' ? 'stateFrom' : 'stateTo');
} }
const config = stateConfig[baseState] || { iconName: 'exclamation-triangle', tooltipContent: 'Unknown State' }; let config = stateConfig[baseState] ?? unknownState;
if (state.includes('Paused')) {
config = stateConfig.Paused;
}
return ( return (
<div <Button variant="secondary" fill="text" size="sm" onClick={() => onStateClick()} tooltip={config.tooltipContent}>
onClick={onStateClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onStateClick();
}
}}
className={styles.state}
role="button"
tabIndex={0}
>
<StateIcon {...config} showLabel={showLabel} /> <StateIcon {...config} showLabel={showLabel} />
</div> </Button>
); );
} }
@ -426,6 +421,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer',
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`, padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
flexWrap: 'nowrap', flexWrap: 'nowrap',
'&:hover': { '&:hover': {
@ -438,26 +434,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
notCollapsedHeader: css({ notCollapsedHeader: css({
borderBottom: 'none', borderBottom: 'none',
}), }),
collapseToggle: css({
background: 'none',
border: 'none',
marginTop: `-${theme.spacing(1)}`,
marginBottom: `-${theme.spacing(1)}`,
svg: {
marginBottom: 0,
},
}),
normalColor: css({
fill: theme.colors.success.text,
}),
warningColor: css({
fill: theme.colors.warning.text,
}),
alertingColor: css({
fill: theme.colors.error.text,
}),
timeCol: css({ timeCol: css({
width: '150px', width: '150px',
}), }),
@ -474,14 +450,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
paddingRight: theme.spacing(2), paddingRight: theme.spacing(2),
flex: 1, flex: 1,
}), }),
alertName: css({
whiteSpace: 'nowrap',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: 'block',
color: theme.colors.text.link,
}),
expandedRow: css({ expandedRow: css({
padding: theme.spacing(2), padding: theme.spacing(2),
marginLeft: theme.spacing(2), marginLeft: theme.spacing(2),
@ -493,12 +461,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
opacity: 0.8, opacity: 0.8,
}, },
}), }),
state: css({
'&:hover': {
opacity: 0.8,
cursor: 'pointer',
},
}),
headerWrapper: css({ headerWrapper: css({
borderBottom: `1px solid ${theme.colors.border.weak}`, borderBottom: `1px solid ${theme.colors.border.weak}`,
}), }),
@ -601,9 +563,6 @@ function useRuleHistoryRecords(
const shouldFilterByLabels = !isEmpty(filterMatchers); const shouldFilterByLabels = !isEmpty(filterMatchers);
const labelsMatch = const labelsMatch =
shouldFilterByLabels && !isEmpty(line.labels) ? labelsMatchMatchers(line.labels, filterMatchers) : true; shouldFilterByLabels && !isEmpty(line.labels) ? labelsMatchMatchers(line.labels, filterMatchers) : true;
if (!isGrafanaAlertState(line.current) || !isGrafanaAlertState(line.previous)) {
return acc;
}
const baseStateTo = mapStateWithReasonToBaseState(line.current); const baseStateTo = mapStateWithReasonToBaseState(line.current);
const baseStateFrom = mapStateWithReasonToBaseState(line.previous); const baseStateFrom = mapStateWithReasonToBaseState(line.previous);

@ -12,7 +12,7 @@ import {
getDisplayProcessor, getDisplayProcessor,
} from '@grafana/data'; } from '@grafana/data';
import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers'; import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
import { isGrafanaAlertState, mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto'; import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
import { labelsMatchMatchers } from '../../../utils/alertmanager'; import { labelsMatchMatchers } from '../../../utils/alertmanager';
import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers'; import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
@ -48,10 +48,6 @@ export function historyResultToDataFrame(data: DataFrameJSON): DataFrame[] {
return acc; return acc;
} }
if (!isGrafanaAlertState(line.current)) {
return acc;
}
// we have to filter out by state at that point , because we are going to group by timestamp and these states are going to be lost // we have to filter out by state at that point , because we are going to group by timestamp and these states are going to be lost
const baseStateTo = mapStateWithReasonToBaseState(line.current); const baseStateTo = mapStateWithReasonToBaseState(line.current);
const baseStateFrom = mapStateWithReasonToBaseState(line.previous); const baseStateFrom = mapStateWithReasonToBaseState(line.previous);

Loading…
Cancel
Save