Alerting: Move AlertLabels to alerting package (#111104)
* Alerting: move AlertLabels to alerting package * add story & test for AlertLabel * export label component from alerting package * rename Label to AlertLabel * resolve PR comments * update imports * export types from internal * run yarn i18n-extract * align with WAI-ARIA standards * export AlertLabels props * update failing test * move labels components to alerting unstable * alertlabel updates * update test props * update types and props * update test prop types * update types and props * add role list to AlertLabel parent * update story * ensure color contrast accessibility * move findCommonLabels into AlertLabels component * unset size as small for tag * update logic to get readable colors * make label and value fonts accessible --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { AlertLabel } from './AlertLabel';
|
||||
|
||||
const meta: Meta<typeof AlertLabel> = {
|
||||
component: AlertLabel,
|
||||
title: 'Rules/AlertLabel',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Basic: StoryObj<typeof AlertLabel> = {
|
||||
render: (args: ComponentProps<typeof AlertLabel>) => (
|
||||
<Stack direction="column" alignItems="flex-start" gap={1} role="list">
|
||||
<AlertLabel role="listitem" labelKey="alertname" value="HighErrorRate" />
|
||||
<AlertLabel role="listitem" labelKey="instance" value="web-01" />
|
||||
<AlertLabel role="listitem" labelKey="job" value="nginx" />
|
||||
<AlertLabel role="listitem" value="hello, world!" />
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Sizes: StoryObj<typeof AlertLabel> = {
|
||||
render: (args: ComponentProps<typeof AlertLabel>) => (
|
||||
<Stack direction="column" alignItems="flex-start" gap={1} role="list">
|
||||
<Text>md</Text>
|
||||
<AlertLabel role="listitem" labelKey="zone" value="us-east-1" size="md" />
|
||||
<Text>sm</Text>
|
||||
<AlertLabel role="listitem" labelKey="zone" value="us-east-1" size="sm" />
|
||||
<Text>xs</Text>
|
||||
<AlertLabel role="listitem" labelKey="zone" value="us-east-1" size="xs" />
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Clickable: StoryObj<typeof AlertLabel> = {
|
||||
render: (args: ComponentProps<typeof AlertLabel>) => (
|
||||
<AlertLabel
|
||||
{...args}
|
||||
labelKey="region"
|
||||
value="eu-central-1"
|
||||
onClick={([value, key]) => console.log('clicked', key, value)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIconAndColor: StoryObj<typeof AlertLabel> = {
|
||||
render: (args: ComponentProps<typeof AlertLabel>) => (
|
||||
<Stack direction="column" alignItems="flex-start" gap={1} role="list">
|
||||
<AlertLabel role="listitem" icon="tag-alt" labelKey="setColor" value="#268bd2" color="#268bd2" />
|
||||
<AlertLabel role="listitem" icon="tag-alt" labelKey="setColor" value="#2aa198" color="#2aa198" />
|
||||
<AlertLabel role="listitem" icon="tag-alt" labelKey="colorBy" value="key" colorBy="key" />
|
||||
<AlertLabel role="listitem" icon="tag-alt" labelKey="colorBy" value="value" colorBy="value" />
|
||||
<AlertLabel role="listitem" icon="tag-alt" labelKey="colorBy" value="both" colorBy="both" />
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { AlertLabel } from './AlertLabel';
|
||||
|
||||
describe('Label', () => {
|
||||
it('renders label and value with correct aria-label', () => {
|
||||
render(<AlertLabel labelKey="foo" value="bar" />);
|
||||
|
||||
const item = screen.getByTestId('label-value');
|
||||
expect(item).toBeInTheDocument();
|
||||
expect(item).toHaveAttribute('aria-label', 'foo: bar');
|
||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||
expect(screen.getByText('bar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when clicked', async () => {
|
||||
const onClick = jest.fn();
|
||||
render(<AlertLabel labelKey="env" value="prod" onClick={onClick} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(onClick).toHaveBeenCalledWith(['prod', 'env']);
|
||||
});
|
||||
|
||||
it('calls onClick when Enter is pressed', async () => {
|
||||
const onClick = jest.fn();
|
||||
render(<AlertLabel labelKey="region" value="eu-west-1" onClick={onClick} />);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
await userEvent.type(button, '{enter}');
|
||||
expect(onClick).toHaveBeenCalledWith(['eu-west-1', 'region']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { CSSProperties, HTMLAttributes, useMemo } from 'react';
|
||||
import tinycolor2 from 'tinycolor2';
|
||||
import { MergeExclusive } from 'type-fest';
|
||||
|
||||
import { GrafanaTheme2, IconName } from '@grafana/data';
|
||||
import { Icon, Stack, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export type LabelSize = 'md' | 'sm' | 'xs';
|
||||
|
||||
interface BaseProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'className'> {
|
||||
icon?: IconName;
|
||||
labelKey?: string;
|
||||
value?: string;
|
||||
size?: LabelSize;
|
||||
onClick?: ([value, key]: [string | undefined, string | undefined]) => void;
|
||||
}
|
||||
|
||||
type Props = BaseProps & MergeExclusive<{ color?: string }, { colorBy?: 'key' | 'value' | 'both' }>;
|
||||
|
||||
const AlertLabel = (props: Props) => {
|
||||
const { labelKey, value, icon, color, colorBy, size = 'md', onClick, ...rest } = props;
|
||||
const theColor = getColorFromProps({ color, colorBy, labelKey, value });
|
||||
const styles = useStyles2(getStyles, theColor, size);
|
||||
|
||||
const ariaLabel = `${labelKey}: ${value}`;
|
||||
const keyless = !Boolean(labelKey);
|
||||
|
||||
const innerLabel = useMemo(
|
||||
() => (
|
||||
<Stack direction="row" gap={0} alignItems="stretch">
|
||||
{labelKey && (
|
||||
<div className={styles.label}>
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
{icon && <Icon name={icon} />}
|
||||
{labelKey && (
|
||||
<span className={styles.labelText} title={labelKey.toString()}>
|
||||
{labelKey ?? ''}
|
||||
</span>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(styles.value, keyless && styles.valueWithoutKey)} title={value?.toString()}>
|
||||
{value ?? '-'}
|
||||
</div>
|
||||
</Stack>
|
||||
),
|
||||
[labelKey, styles.label, styles.labelText, styles.value, styles.valueWithoutKey, icon, keyless, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} aria-label={ariaLabel} data-testid="label-value" {...rest}>
|
||||
{onClick && (labelKey || value) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clickable}
|
||||
key={`${labelKey ?? ''}${value ?? ''}`}
|
||||
onClick={() => onClick?.([value ?? '', labelKey ?? ''])}
|
||||
>
|
||||
{innerLabel}
|
||||
</button>
|
||||
) : (
|
||||
innerLabel
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getAccessibleTagColor(name?: string): string | undefined {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const attempts = Array.from({ length: 6 }, (_, i) => name + '-'.repeat(i));
|
||||
const readableAttempt = attempts.find((attempt) => {
|
||||
const candidate = getTagColorsFromName(attempt).color;
|
||||
return (
|
||||
tinycolor2.isReadable(candidate, '#000', { level: 'AA', size: 'small' }) ||
|
||||
tinycolor2.isReadable(candidate, '#fff', { level: 'AA', size: 'small' })
|
||||
);
|
||||
});
|
||||
const chosen = readableAttempt ?? name;
|
||||
return getTagColorsFromName(chosen).color;
|
||||
}
|
||||
|
||||
function getColorFromProps({
|
||||
color,
|
||||
colorBy,
|
||||
labelKey,
|
||||
value,
|
||||
}: Pick<Props, 'color' | 'colorBy' | 'labelKey' | 'value'>) {
|
||||
if (color) {
|
||||
return getAccessibleTagColor(color);
|
||||
}
|
||||
|
||||
if (colorBy === 'key') {
|
||||
return getAccessibleTagColor(labelKey);
|
||||
}
|
||||
|
||||
if (colorBy === 'value') {
|
||||
return getAccessibleTagColor(value);
|
||||
}
|
||||
|
||||
if (colorBy === 'both' && labelKey && value) {
|
||||
return getAccessibleTagColor(labelKey + value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function getReadableFontColor(bg: string, fallback: string): string {
|
||||
// First: explicitly check black
|
||||
if (tinycolor2.isReadable(bg, '#000', { level: 'AA', size: 'small' })) {
|
||||
return '#000';
|
||||
}
|
||||
|
||||
// Then: explicitly check white
|
||||
if (tinycolor2.isReadable(bg, '#fff', { level: 'AA', size: 'small' })) {
|
||||
return '#fff';
|
||||
}
|
||||
|
||||
// Then: try fallback if it’s readable
|
||||
if (tinycolor2.isReadable(bg, fallback, { level: 'AA', size: 'small' })) {
|
||||
return tinycolor2(fallback).toHexString();
|
||||
}
|
||||
|
||||
// Last resort: pick the "most readable", even if not AA-compliant
|
||||
return tinycolor2
|
||||
.mostReadable(bg, ['#000', '#fff', fallback], {
|
||||
includeFallbackColors: true,
|
||||
})
|
||||
.toHexString();
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
|
||||
const backgroundColor = color ?? theme.colors.secondary.main;
|
||||
|
||||
const borderColor = theme.isDark
|
||||
? tinycolor2(backgroundColor).lighten(5).toString()
|
||||
: tinycolor2(backgroundColor).darken(5).toString();
|
||||
|
||||
const valueBackgroundColor = theme.isDark
|
||||
? tinycolor2(backgroundColor).darken(5).toString()
|
||||
: tinycolor2(backgroundColor).lighten(5).toString();
|
||||
|
||||
const labelFontColor = color
|
||||
? getReadableFontColor(backgroundColor, theme.colors.text.primary)
|
||||
: theme.colors.text.primary;
|
||||
|
||||
const valueFontColor = color
|
||||
? getReadableFontColor(valueBackgroundColor, theme.colors.text.primary)
|
||||
: theme.colors.text.primary;
|
||||
|
||||
let padding: CSSProperties['padding'] = theme.spacing(0.33, 1);
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
padding = theme.spacing(0.2, 0.6);
|
||||
break;
|
||||
case 'xs':
|
||||
padding = theme.spacing(0, 0.5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
wrapper: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
borderRadius: theme.shape.borderRadius(2),
|
||||
}),
|
||||
labelText: css({
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '300px',
|
||||
}),
|
||||
label: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: labelFontColor,
|
||||
|
||||
padding: padding,
|
||||
background: backgroundColor,
|
||||
|
||||
border: `solid 1px ${borderColor}`,
|
||||
borderTopLeftRadius: theme.shape.borderRadius(2),
|
||||
borderBottomLeftRadius: theme.shape.borderRadius(2),
|
||||
}),
|
||||
clickable: css({
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
value: css({
|
||||
color: valueFontColor,
|
||||
padding: padding,
|
||||
background: valueBackgroundColor,
|
||||
border: `solid 1px ${borderColor}`,
|
||||
borderLeft: 'none',
|
||||
borderTopRightRadius: theme.shape.borderRadius(2),
|
||||
borderBottomRightRadius: theme.shape.borderRadius(2),
|
||||
whiteSpace: 'pre',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '300px',
|
||||
}),
|
||||
valueWithoutKey: css({
|
||||
borderTopLeftRadius: theme.shape.borderRadius(2),
|
||||
borderBottomLeftRadius: theme.shape.borderRadius(2),
|
||||
borderLeft: `solid 1px ${borderColor}`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export { AlertLabel };
|
||||
export type AlertLabelProps = Props;
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import { AlertLabels } from './AlertLabels';
|
||||
|
||||
const meta: Meta<typeof AlertLabels> = {
|
||||
component: AlertLabels,
|
||||
title: 'Rules/AlertLabels',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Basic: StoryObj<typeof AlertLabels> = {
|
||||
render: (args: ComponentProps<typeof AlertLabels>) => (
|
||||
<Stack direction="column" alignItems="flex-start">
|
||||
<AlertLabels
|
||||
{...args}
|
||||
labels={{ alertname: 'HighErrorRate', instance: 'web-01', job: 'nginx', zone: 'us-east-1' }}
|
||||
/>
|
||||
<AlertLabels
|
||||
{...args}
|
||||
size="sm"
|
||||
labels={{ alertname: 'HighCPU', instance: 'db-02', job: 'postgres', zone: 'us-west-2' }}
|
||||
/>
|
||||
<AlertLabels
|
||||
{...args}
|
||||
size="xs"
|
||||
labels={{ alertname: 'DiskFull', instance: 'cache-3', job: 'redis', zone: 'eu-central-1' }}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithCommonLabels: StoryObj<typeof AlertLabels> = {
|
||||
render: (args: ComponentProps<typeof AlertLabels>) => (
|
||||
<AlertLabels
|
||||
{...args}
|
||||
labels={{ foo: 'bar', bar: 'baz', baz: 'qux' }}
|
||||
displayCommonLabels
|
||||
labelSets={[
|
||||
{ foo: 'bar', bar: 'baz', baz: 'qux' },
|
||||
{ foo: 'bar', baz: 'qux', quux: 'corge' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
};
|
||||
+2
-2
@@ -6,9 +6,9 @@ import { AlertLabels } from './AlertLabels';
|
||||
describe('AlertLabels', () => {
|
||||
it('should toggle show / hide common labels', async () => {
|
||||
const labels = { foo: 'bar', bar: 'baz', baz: 'qux' };
|
||||
const commonLabels = { foo: 'bar', baz: 'qux' };
|
||||
const another = { foo: 'bar', baz: 'qux', extra: 'z' };
|
||||
|
||||
render(<AlertLabels labels={labels} commonLabels={commonLabels} />);
|
||||
render(<AlertLabels labels={labels} displayCommonLabels labelSets={[labels, another]} />);
|
||||
expect(screen.getByText('+2 common labels')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
@@ -0,0 +1,97 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { chain } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { findCommonLabels, isPrivateLabel } from '../../utils/labels';
|
||||
|
||||
import { AlertLabel, LabelSize } from './AlertLabel';
|
||||
|
||||
export interface AlertLabelsProps {
|
||||
labels: Record<string, string>;
|
||||
displayCommonLabels?: boolean;
|
||||
labelSets?: Array<Record<string, string>>;
|
||||
size?: LabelSize;
|
||||
onClick?: ([value, key]: [string | undefined, string | undefined]) => void;
|
||||
}
|
||||
|
||||
export const AlertLabels = ({ labels, displayCommonLabels, labelSets, size, onClick }: AlertLabelsProps) => {
|
||||
const styles = useStyles2(getStyles, size);
|
||||
const [showCommonLabels, setShowCommonLabels] = useState(false);
|
||||
|
||||
const computedCommonLabels =
|
||||
displayCommonLabels && Array.isArray(labelSets) && labelSets.length > 1 ? findCommonLabels(labelSets) : {};
|
||||
|
||||
const labelsToShow = chain(labels)
|
||||
.toPairs()
|
||||
.reject(isPrivateLabel)
|
||||
.reject(([key]) => (showCommonLabels ? false : key in computedCommonLabels))
|
||||
.value();
|
||||
|
||||
const commonLabelsCount = Object.keys(computedCommonLabels).length;
|
||||
const hasCommonLabels = commonLabelsCount > 0;
|
||||
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} role="list" aria-label={t('alerting.alert-labels.aria-label-labels', 'Labels')}>
|
||||
{labelsToShow.map(([label, value]) => {
|
||||
return (
|
||||
<AlertLabel
|
||||
key={label + value}
|
||||
size={size}
|
||||
labelKey={label}
|
||||
value={value}
|
||||
colorBy="key"
|
||||
onClick={onClick}
|
||||
role="listitem"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{!showCommonLabels && hasCommonLabels && (
|
||||
<div role="listitem">
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={() => setShowCommonLabels(true)}
|
||||
tooltip={tooltip}
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
<Trans i18nKey="alerting.alert-labels.common-labels-count" count={commonLabelsCount}>
|
||||
+{'{{count}}'} common labels
|
||||
</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{showCommonLabels && hasCommonLabels && (
|
||||
<div role="listitem">
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={() => setShowCommonLabels(false)}
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
<Trans i18nKey="alert-labels.button.hide">Hide common labels</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, size?: LabelSize) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
|
||||
gap: size === 'md' ? theme.spacing() : theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export const GRAFANA_ORIGIN_LABEL = '__grafana_origin';
|
||||
|
||||
export function isPrivateLabelKey(labelKey: string) {
|
||||
return (labelKey.startsWith('__') && labelKey.endsWith('__')) || labelKey === GRAFANA_ORIGIN_LABEL;
|
||||
}
|
||||
|
||||
export const isPrivateLabel = ([key, _]: [string, string]) => isPrivateLabelKey(key);
|
||||
|
||||
/**
|
||||
* Returns a map labels that are common to the given label sets.
|
||||
*/
|
||||
export function findCommonLabels(labelSets: Array<Record<string, string>>): Record<string, string> {
|
||||
if (!Array.isArray(labelSets) || labelSets.length === 0) {
|
||||
return {};
|
||||
}
|
||||
return labelSets.reduce(
|
||||
(acc, labels) => {
|
||||
if (!labels) {
|
||||
throw new Error('Need parsed labels to find common labels.');
|
||||
}
|
||||
// Remove incoming labels that are missing or not matching in value
|
||||
Object.keys(labels).forEach((key) => {
|
||||
if (acc[key] === undefined || acc[key] !== labels[key]) {
|
||||
delete acc[key];
|
||||
}
|
||||
});
|
||||
// Remove common labels that are missing from incoming label set
|
||||
Object.keys(acc).forEach((key) => {
|
||||
if (labels[key] === undefined) {
|
||||
delete acc[key];
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ ...labelSets[0] }
|
||||
);
|
||||
}
|
||||
@@ -7,5 +7,9 @@
|
||||
* Breaking changes should be avoided to maintain backwards compatibility for consumers of this package.
|
||||
*/
|
||||
|
||||
export { AlertLabels } from './grafana/rules/components/labels/AlertLabels';
|
||||
export { AlertLabel } from './grafana/rules/components/labels/AlertLabel';
|
||||
// keep label utils internal to the app for now
|
||||
|
||||
// This is a dummy export so typescript doesn't error importing an "empty module"
|
||||
export const index = {};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
{
|
||||
"alert-labels": {
|
||||
"button": {
|
||||
"hide": "Hide common labels",
|
||||
"show": {
|
||||
"tooltip": "Show common labels"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerting": {
|
||||
"alert-labels": {
|
||||
"aria-label-labels": "Labels",
|
||||
"common-labels-count_one": "+{{count}} common labels",
|
||||
"common-labels-count_other": "+{{count}} common labels"
|
||||
},
|
||||
"paused-badge": {
|
||||
"paused": "Paused"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export * from './grafana/notificationPolicies/types';
|
||||
// Rules
|
||||
export { StateText } from './grafana/rules/components/state/StateText';
|
||||
export { StateIcon } from './grafana/rules/components/state/StateIcon';
|
||||
export { AlertLabel, type AlertLabelProps } from './grafana/rules/components/labels/AlertLabel';
|
||||
export { AlertLabels, type AlertLabelsProps } from './grafana/rules/components/labels/AlertLabels';
|
||||
|
||||
// Matchers
|
||||
export { type LabelMatcher, type Label } from './grafana/matchers/types';
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useMemo } from 'react';
|
||||
import { Navigate } from 'react-router-dom-v5-compat';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, isFetchError } from '@grafana/runtime';
|
||||
import { Alert, Card, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { AlertLabels } from './components/AlertLabels';
|
||||
import { RuleViewerLayout } from './components/rule-viewer/RuleViewerLayout';
|
||||
import { useCloudCombinedRulesMatching } from './hooks/useCombinedRule';
|
||||
import { getRulesSourceByName } from './utils/datasource';
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { chain } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isPrivateLabel } from '../utils/labels';
|
||||
|
||||
import { Label, LabelSize } from './Label';
|
||||
|
||||
interface Props {
|
||||
labels: Record<string, string>;
|
||||
commonLabels?: Record<string, string>;
|
||||
size?: LabelSize;
|
||||
onClick?: (label: string, value: string) => void;
|
||||
}
|
||||
|
||||
export const AlertLabels = ({ labels, commonLabels = {}, size, onClick }: Props) => {
|
||||
const styles = useStyles2(getStyles, size);
|
||||
const [showCommonLabels, setShowCommonLabels] = useState(false);
|
||||
|
||||
const labelsToShow = chain(labels)
|
||||
.toPairs()
|
||||
.reject(isPrivateLabel)
|
||||
.reject(([key]) => (showCommonLabels ? false : key in commonLabels))
|
||||
.value();
|
||||
|
||||
const commonLabelsCount = Object.keys(commonLabels).length;
|
||||
const hasCommonLabels = commonLabelsCount > 0;
|
||||
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} role="list" aria-label={t('alerting.alert-labels.aria-label-labels', 'Labels')}>
|
||||
{labelsToShow.map(([label, value]) => {
|
||||
return (
|
||||
<Label
|
||||
key={label + value}
|
||||
size={size}
|
||||
label={label}
|
||||
value={value}
|
||||
color={getLabelColor(label)}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{!showCommonLabels && hasCommonLabels && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={() => setShowCommonLabels(true)}
|
||||
tooltip={tooltip}
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
<Trans i18nKey="alerting.alert-labels.common-labels-count" count={commonLabelsCount}>
|
||||
+{'{{count}}'} common labels
|
||||
</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{showCommonLabels && hasCommonLabels && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
onClick={() => setShowCommonLabels(false)}
|
||||
tooltipPlacement="top"
|
||||
size="sm"
|
||||
>
|
||||
<Trans i18nKey="alert-labels.button.hide">Hide common labels</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getLabelColor(input: string): string {
|
||||
return getTagColorsFromName(input).color;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, size?: LabelSize) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
|
||||
gap: size === 'md' ? theme.spacing() : theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,148 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { CSSProperties, ReactNode, useMemo } from 'react';
|
||||
import tinycolor2 from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme2, IconName } from '@grafana/data';
|
||||
import { Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export type LabelSize = 'md' | 'sm' | 'xs';
|
||||
|
||||
interface Props {
|
||||
icon?: IconName;
|
||||
label?: ReactNode;
|
||||
value: ReactNode;
|
||||
color?: string;
|
||||
size?: LabelSize;
|
||||
onClick?: (label: string, value: string) => void;
|
||||
}
|
||||
|
||||
// TODO allow customization with color prop
|
||||
const Label = ({ label, value, icon, color, size = 'md', onClick }: Props) => {
|
||||
const styles = useStyles2(getStyles, color, size);
|
||||
const ariaLabel = `${label}: ${value}`;
|
||||
const labelStr = label?.toString() ?? '';
|
||||
const valueStr = value?.toString() ?? '';
|
||||
|
||||
const innerLabel = useMemo(
|
||||
() => (
|
||||
<Stack direction="row" gap={0} alignItems="stretch">
|
||||
<div className={styles.label}>
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
{icon && <Icon name={icon} />}
|
||||
{label && (
|
||||
<span className={styles.labelText} title={label.toString()}>
|
||||
{label ?? ''}
|
||||
</span>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className={styles.value} title={value?.toString()}>
|
||||
{value ?? '-'}
|
||||
</div>
|
||||
</Stack>
|
||||
),
|
||||
[icon, label, value, styles]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} role="listitem" aria-label={ariaLabel} data-testid="label-value">
|
||||
{onClick ? (
|
||||
<div
|
||||
className={styles.clickable}
|
||||
role="button" // role="button" and tabIndex={0} is needed for keyboard navigation
|
||||
tabIndex={0} // Make it focusable
|
||||
key={labelStr + valueStr}
|
||||
onClick={() => onClick(labelStr, valueStr)}
|
||||
onKeyDown={(e) => {
|
||||
// needed for accessiblity: handle keyboard navigation
|
||||
if (e.key === 'Enter') {
|
||||
onClick(labelStr, valueStr);
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{innerLabel}
|
||||
</div>
|
||||
) : (
|
||||
innerLabel
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
|
||||
const backgroundColor = color ?? theme.colors.secondary.main;
|
||||
|
||||
const borderColor = theme.isDark
|
||||
? tinycolor2(backgroundColor).lighten(5).toString()
|
||||
: tinycolor2(backgroundColor).darken(5).toString();
|
||||
|
||||
const valueBackgroundColor = theme.isDark
|
||||
? tinycolor2(backgroundColor).darken(5).toString()
|
||||
: tinycolor2(backgroundColor).lighten(5).toString();
|
||||
|
||||
const fontColor = color
|
||||
? tinycolor2.mostReadable(backgroundColor, ['#000', '#fff']).toString()
|
||||
: theme.colors.text.primary;
|
||||
|
||||
let padding: CSSProperties['padding'] = theme.spacing(0.33, 1);
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
padding = theme.spacing(0.2, 0.6);
|
||||
break;
|
||||
case 'xs':
|
||||
padding = theme.spacing(0, 0.5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
wrapper: css({
|
||||
color: fontColor,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
|
||||
borderRadius: theme.shape.borderRadius(2),
|
||||
}),
|
||||
labelText: css({
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '300px',
|
||||
}),
|
||||
label: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'inherit',
|
||||
|
||||
padding: padding,
|
||||
background: backgroundColor,
|
||||
|
||||
border: `solid 1px ${borderColor}`,
|
||||
borderTopLeftRadius: theme.shape.borderRadius(2),
|
||||
borderBottomLeftRadius: theme.shape.borderRadius(2),
|
||||
}),
|
||||
clickable: css({
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
value: css({
|
||||
color: 'inherit',
|
||||
padding: padding,
|
||||
background: valueBackgroundColor,
|
||||
border: `solid 1px ${borderColor}`,
|
||||
borderLeft: 'none',
|
||||
borderTopRightRadius: theme.shape.borderRadius(2),
|
||||
borderBottomRightRadius: theme.shape.borderRadius(2),
|
||||
whiteSpace: 'pre',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '300px',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export { Label };
|
||||
@@ -1,13 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Stack, TextLink, useStyles2 } from '@grafana/ui';
|
||||
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { createContactPointSearchLink } from '../../utils/misc';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { MetaText } from '../MetaText';
|
||||
|
||||
|
||||
+1
-1
@@ -1,12 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
|
||||
import { AmAlertStateTag } from '../silences/AmAlertStateTag';
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as React from 'react';
|
||||
import { FC, Fragment, ReactNode, useState } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { AlertLabel } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import {
|
||||
@@ -42,7 +43,6 @@ import { InheritableProperties, getInheritedProperties } from '../../utils/notif
|
||||
import { InsertPosition } from '../../utils/routeTree';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { PopupCard } from '../HoverCard';
|
||||
import { Label } from '../Label';
|
||||
import { MetaText } from '../MetaText';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { Spacer } from '../Spacer';
|
||||
@@ -720,7 +720,13 @@ const InheritedProperties: FC<{ properties: InheritableProperties }> = ({ proper
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Label key={key} label={routePropertyToLabel(key)} value={routePropertyToValue(key, value)} />;
|
||||
return (
|
||||
<AlertLabel
|
||||
key={key}
|
||||
labelKey={routePropertyToLabel(key)}
|
||||
value={String(routePropertyToValue(key, value))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
}
|
||||
@@ -917,13 +923,13 @@ function getContactPointErrors(contactPoint: string, contactPointsState: Receive
|
||||
const notifierErrors = notifierStatuses
|
||||
.filter((status) => status.lastNotifyAttemptError)
|
||||
.map((status) => (
|
||||
<Label
|
||||
<AlertLabel
|
||||
icon="at"
|
||||
key={uniqueId()}
|
||||
label={t('alerting.contact-point-errors.label-contact-point', 'Contact Point › {{name}}', {
|
||||
value={String(status.lastNotifyAttemptError)}
|
||||
labelKey={t('alerting.contact-point-errors.label-contact-point', 'Contact Point › {{name}}', {
|
||||
name: status.name,
|
||||
})}
|
||||
value={status.lastNotifyAttemptError}
|
||||
/>
|
||||
));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { Controller, FormProvider, useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, ComboboxOption, Field, InlineLabel, Input, Space, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
@@ -13,7 +14,6 @@ import { KBObjectArray, RuleFormType, RuleFormValues } from '../../../types/rule
|
||||
import { isPrivateLabelKey } from '../../../utils/labels';
|
||||
import { isRecordingRuleByType } from '../../../utils/rules';
|
||||
import AlertLabelDropdown from '../../AlertLabelDropdown';
|
||||
import { AlertLabels } from '../../AlertLabels';
|
||||
import { NeedHelpInfo } from '../NeedHelpInfo';
|
||||
import { useGetLabelsFromDataSourceName } from '../useAlertRuleSuggestions';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { chain, truncate } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { StateText } from '@grafana/alerting/unstable';
|
||||
import { AlertLabels, StateText } from '@grafana/alerting/unstable';
|
||||
import { NavModelItem, UrlQueryValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import {
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
prometheusRuleType,
|
||||
rulerRuleType,
|
||||
} from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
import { WithReturnButton } from '../WithReturnButton';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PluginExtensionPoints, dateTime, findCommonLabels } from '@grafana/data';
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { PluginExtensionPoints, dateTime } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
|
||||
|
||||
import { alertInstanceKey } from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { AlertInstanceExtensionPoint } from '../extensions/AlertInstanceExtensionPoint';
|
||||
|
||||
@@ -19,34 +20,72 @@ interface Props {
|
||||
footerRow?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AlertWithCommonLabels extends Alert {
|
||||
commonLabels?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface RuleAndAlert {
|
||||
rule?: CombinedRule;
|
||||
alert: AlertWithCommonLabels;
|
||||
alert: Alert;
|
||||
}
|
||||
|
||||
type AlertTableColumnProps = DynamicTableColumnProps<RuleAndAlert>;
|
||||
type AlertTableItemProps = DynamicTableItemProps<RuleAndAlert>;
|
||||
|
||||
export const AlertInstancesTable = ({ rule, instances, pagination, footerRow }: Props) => {
|
||||
const commonLabels = useMemo(() => {
|
||||
// only compute the common labels if we have more than 1 instance, if we don't then that single instance
|
||||
// will have the complete set of common labels and no unique ones
|
||||
return instances.length > 1 ? findCommonLabels(instances.map((instance) => instance.labels)) : {};
|
||||
}, [instances]);
|
||||
|
||||
const items = useMemo(
|
||||
(): AlertTableItemProps[] =>
|
||||
instances.map((instance) => ({
|
||||
data: { rule, alert: { ...instance, commonLabels } },
|
||||
data: { rule, alert: instance },
|
||||
id: alertInstanceKey(instance),
|
||||
})),
|
||||
[commonLabels, instances, rule]
|
||||
[instances, rule]
|
||||
);
|
||||
|
||||
const columns: AlertTableColumnProps[] = [
|
||||
{
|
||||
id: 'state',
|
||||
label: t('alerting.alert-instances-table.state', 'State'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { state },
|
||||
},
|
||||
}) => <AlertStateTag state={state} />,
|
||||
size: '95px',
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: t('alerting.alert-instances-table.labels', 'Labels'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { labels },
|
||||
},
|
||||
}) => <AlertLabels labels={labels} labelSets={instances.map((i) => i.labels)} displayCommonLabels size="sm" />,
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: t('alerting.alert-instances-table.created', 'Created'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { activeAt },
|
||||
},
|
||||
}) => <>{activeAt.startsWith('0001') ? '-' : dateTime(activeAt).format('YYYY-MM-DD HH:mm:ss')}</>,
|
||||
size: '150px',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
label: '',
|
||||
renderCell: ({ data: { alert, rule } }) => (
|
||||
<AlertInstanceExtensionPoint
|
||||
rule={rule}
|
||||
instance={alert}
|
||||
extensionPointId={PluginExtensionPoints.AlertInstanceAction}
|
||||
key="alert-instance-extension-point"
|
||||
/>
|
||||
),
|
||||
size: '40px',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DynamicTable
|
||||
cols={columns}
|
||||
@@ -58,51 +97,3 @@ export const AlertInstancesTable = ({ rule, instances, pagination, footerRow }:
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: AlertTableColumnProps[] = [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { state },
|
||||
},
|
||||
}) => <AlertStateTag state={state} />,
|
||||
size: '95px',
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { labels, commonLabels },
|
||||
},
|
||||
}) => <AlertLabels labels={labels} commonLabels={commonLabels} size="sm" />,
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({
|
||||
data: {
|
||||
alert: { activeAt },
|
||||
},
|
||||
}) => <>{activeAt.startsWith('0001') ? '-' : dateTime(activeAt).format('YYYY-MM-DD HH:mm:ss')}</>,
|
||||
size: '150px',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
label: '',
|
||||
renderCell: ({ data: { alert, rule } }) => (
|
||||
<AlertInstanceExtensionPoint
|
||||
rule={rule}
|
||||
instance={alert}
|
||||
extensionPointId={PluginExtensionPoints.AlertInstanceAction}
|
||||
key="alert-instance-extension-point"
|
||||
/>
|
||||
),
|
||||
size: '40px',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
@@ -10,7 +11,6 @@ import { usePendingPeriod } from '../../hooks/rules/usePendingPeriod';
|
||||
import { useCleanAnnotations } from '../../utils/annotations';
|
||||
import { prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
import { isNullDate } from '../../utils/time';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
|
||||
import { RuleDetailsAnnotations } from './RuleDetailsAnnotations';
|
||||
|
||||
+5
-3
@@ -3,6 +3,7 @@ import { ReactElement, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, IconName, TimeRange } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import {
|
||||
@@ -31,7 +32,6 @@ import { usePagination } from '../../../hooks/usePagination';
|
||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
import { createRelativeUrl } from '../../../utils/url';
|
||||
import { AlertLabels } from '../../AlertLabels';
|
||||
import { CollapseToggle } from '../../CollapseToggle';
|
||||
import { LogRecord } from '../state-history/common';
|
||||
|
||||
@@ -210,8 +210,10 @@ interface EventRowProps {
|
||||
function EventRow({ record, addFilter, timeRange, hideAlertRuleColumn }: EventRowProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
function onLabelClick(label: string, value: string) {
|
||||
addFilter(label, value, 'label');
|
||||
function onLabelClick([value, label]: [string | undefined, string | undefined]) {
|
||||
if (label && value) {
|
||||
addFilter(label, value, 'label');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ import { formatDistanceToNowStrict } from 'date-fns';
|
||||
import { groupBy, uniqueId } from 'lodash';
|
||||
import { Fragment, memo, useEffect, useRef } from 'react';
|
||||
|
||||
import { AlertLabel } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, dateTimeFormat } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Icon, Stack, TagList, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaAlertState, mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { Label } from '../../Label';
|
||||
import { AlertStateTag } from '../AlertStateTag';
|
||||
|
||||
import { ErrorMessageRow } from './ErrorMessageRow';
|
||||
@@ -182,7 +182,7 @@ const AlertInstanceValues = memo(({ record }: { record: Record<string, number> }
|
||||
return (
|
||||
<>
|
||||
{values.map(([key, value]) => (
|
||||
<Label key={key} label={key} value={value} />
|
||||
<AlertLabel key={key} labelKey={key} value={String(value)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
+1
-1
@@ -4,13 +4,13 @@ import * as React from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { DataFrame, GrafanaTheme2, TimeRange, dateTime } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||
import { AlertLabels } from '../../AlertLabels';
|
||||
import { PopupCard } from '../../HoverCard';
|
||||
|
||||
import { LogRecordViewerByTimestamp } from './LogRecordViewer';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
|
||||
import { AmAlertStateTag } from './AmAlertStateTag';
|
||||
|
||||
+1
-1
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce, useDeepCompareEffect } from 'react-use';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2, dateTime } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Badge, Icon, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
@@ -12,7 +13,6 @@ import { AlertmanagerAlert, Matcher, MatcherOperator } from 'app/plugins/datasou
|
||||
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { isNullDate } from '../../utils/time';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
|
||||
import { AmAlertStateTag } from './AmAlertStateTag';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import pluralize from 'pluralize';
|
||||
import { ReactNode, forwardRef, memo, useEffect, useId } from 'react';
|
||||
|
||||
import { StateIcon } from '@grafana/alerting/unstable';
|
||||
import { AlertLabels, StateIcon } from '@grafana/alerting/unstable';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
@@ -10,7 +10,6 @@ import { Rule, RuleGroupIdentifierV2, RuleHealth, RulesSourceIdentifier } from '
|
||||
import { Labels, PromAlertingRuleState, RulerRuleDTO, RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logError } from '../../Analytics';
|
||||
import { AlertLabels } from '../../components/AlertLabels';
|
||||
import ConditionalWrap from '../../components/ConditionalWrap';
|
||||
import { MetaText } from '../../components/MetaText';
|
||||
import { ProvisioningBadge } from '../../components/Provisioning';
|
||||
|
||||
@@ -364,14 +364,6 @@
|
||||
"tooltip-assigned-role": "A licensed role will be assigned when this user signs in"
|
||||
}
|
||||
},
|
||||
"alert-labels": {
|
||||
"button": {
|
||||
"hide": "Hide common labels",
|
||||
"show": {
|
||||
"tooltip": "Show common labels"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerting": {
|
||||
"active-timing-fields": {
|
||||
"am-active-timing-select-label-active-timings": "Active timings"
|
||||
@@ -480,17 +472,17 @@
|
||||
"alert-instance-state-filter": {
|
||||
"state": "State"
|
||||
},
|
||||
"alert-instances-table": {
|
||||
"created": "Created",
|
||||
"labels": "Labels",
|
||||
"state": "State"
|
||||
},
|
||||
"alert-label": {
|
||||
"tooltip-remove-label": "Remove label"
|
||||
},
|
||||
"alert-label-dropdown": {
|
||||
"placeholder-select": "Choose {{type}}"
|
||||
},
|
||||
"alert-labels": {
|
||||
"aria-label-labels": "Labels",
|
||||
"common-labels-count_one": "+{{count}} common labels",
|
||||
"common-labels-count_other": "+{{count}} common labels"
|
||||
},
|
||||
"alert-manager-manual-routing": {
|
||||
"label-muting-grouping-and-timings-optional": "Muting, grouping and timings (optional)",
|
||||
"title-muting-grouping-and-timings": "Muting, grouping, and timings"
|
||||
|
||||
Reference in New Issue
Block a user