Alerting: Add state components to package (#110781)
This commit is contained in:
@@ -58,7 +58,8 @@
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts",
|
||||
"prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=testing,unstable node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json && rimraf ./unstable ./testing"
|
||||
"postpack": "mv package.json.bak package.json && rimraf ./unstable ./testing",
|
||||
"i18n-extract": "i18next --config src/locales/i18next-parser.config.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/test-utils": "workspace:*",
|
||||
@@ -69,6 +70,7 @@
|
||||
"@types/lodash": "^4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"i18next": "^25.5.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-redux": "^9.2.0",
|
||||
@@ -90,6 +92,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"@grafana/i18n": "12.2.0-pre",
|
||||
"fishery": "^2.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { simpleContactPointsListScenario, withErrorScenario } from './ContactPoi
|
||||
|
||||
const meta: Meta<typeof ContactPointSelector> = {
|
||||
component: ContactPointSelector,
|
||||
title: 'ContactPointSelector',
|
||||
title: 'Contact Points/ContactPointSelector',
|
||||
decorators: defaultDecorators,
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface DotStylesProps {
|
||||
color: 'success' | 'error' | 'warning' | 'unknown';
|
||||
}
|
||||
|
||||
const StateDot = ({ color }: DotStylesProps) => {
|
||||
const styles = useStyles2(getDotStyles, { color });
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5}>
|
||||
<div className={styles.dot} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getDotStyles = (theme: GrafanaTheme2, { color }: DotStylesProps) => {
|
||||
const size = theme.spacing(1.25);
|
||||
const outlineSize = `calc(${size} / 2.5)`;
|
||||
|
||||
const errorStyle = color === 'error';
|
||||
const successStyle = color === 'success';
|
||||
const warningStyle = color === 'warning';
|
||||
|
||||
return {
|
||||
dot: css(
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
|
||||
backgroundColor: theme.colors.secondary.shade,
|
||||
outline: `solid ${outlineSize} ${theme.colors.secondary.transparent}`,
|
||||
margin: outlineSize,
|
||||
},
|
||||
successStyle &&
|
||||
css({
|
||||
backgroundColor: theme.colors.success.main,
|
||||
outlineColor: theme.colors.success.transparent,
|
||||
}),
|
||||
warningStyle &&
|
||||
css({
|
||||
backgroundColor: theme.colors.warning.main,
|
||||
outlineColor: theme.colors.warning.transparent,
|
||||
}),
|
||||
errorStyle &&
|
||||
css({
|
||||
backgroundColor: theme.colors.error.main,
|
||||
outlineColor: theme.colors.error.transparent,
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export { StateDot };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ArgTypes } from '@storybook/blocks';
|
||||
import { StateIcon } from './StateIcon';
|
||||
|
||||
# StateIcon
|
||||
|
||||
A component for showing the state and health of a rule. This components supports pending operations for the rule.
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { StateIcon } from './StateIcon';
|
||||
import mdx from './StateIcon.mdx';
|
||||
|
||||
const meta: Meta<typeof StateIcon> = {
|
||||
component: StateIcon,
|
||||
title: 'Rules/StateIcon',
|
||||
decorators: [],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const StoryRenderFn: StoryFn<ComponentProps<typeof StateIcon>> = (args) => {
|
||||
return <StateIcon {...args} />;
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof StateIcon>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: StoryRenderFn,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { render, screen } from '../../../../../tests/test-utils';
|
||||
|
||||
import { StateIcon } from './StateIcon';
|
||||
|
||||
describe('StateIcon', () => {
|
||||
it('should render the icon for "normal" state', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="normal" />);
|
||||
const icon = screen.getByLabelText('Normal');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Normal');
|
||||
});
|
||||
|
||||
it('should render the icon for "firing" state', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="firing" />);
|
||||
const icon = screen.getByLabelText('Firing');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Firing');
|
||||
});
|
||||
|
||||
// Health takes precedence over state
|
||||
it('should show "Failed to evaluate rule" when health is "error", ignoring state', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="normal" health="error" />);
|
||||
const icon = screen.getByLabelText('Failed to evaluate rule');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Failed to evaluate rule');
|
||||
});
|
||||
|
||||
it('should show "Insufficient data" when health is "nodata", ignoring state', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="firing" health="nodata" />);
|
||||
const icon = screen.getByLabelText('Insufficient data');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Insufficient data');
|
||||
});
|
||||
|
||||
// isPaused takes precedence over health and state
|
||||
it('should show "Paused" when isPaused is true, ignoring health and state', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="firing" health="error" isPaused />);
|
||||
const icon = screen.getByLabelText('Paused');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Paused');
|
||||
});
|
||||
|
||||
// operation takes precedence over all
|
||||
it('should show "Creating" when operation is "creating", ignoring other props', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="firing" health="error" isPaused operation="creating" />);
|
||||
const icon = screen.getByLabelText('Creating');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Creating');
|
||||
});
|
||||
|
||||
it('should show "Deleting" when operation is "deleting", ignoring other props', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon state="normal" operation="deleting" />);
|
||||
const icon = screen.getByLabelText('Deleting');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Deleting');
|
||||
});
|
||||
|
||||
it('should show "Recording" when recording is true', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon type="recording" />);
|
||||
const icon = screen.getByLabelText('Recording');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Recording');
|
||||
});
|
||||
|
||||
it('should show "Failed to evaluate rule" when Recording health is "error"', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StateIcon type="recording" health="error" />);
|
||||
const icon = screen.getByLabelText('Failed to evaluate rule');
|
||||
await user.hover(icon);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Failed to evaluate rule');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { css, keyframes } from '@emotion/css';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { ComponentProps, memo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, type IconName, Text, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import type { Health, State, Type } from './types';
|
||||
|
||||
type TextProps = ComponentProps<typeof Text>;
|
||||
|
||||
interface CommonStateIconsProps {
|
||||
type?: Type;
|
||||
health?: Health;
|
||||
isPaused?: boolean;
|
||||
operation?: RuleOperation;
|
||||
}
|
||||
|
||||
interface AlertingStateIconProps extends CommonStateIconsProps {
|
||||
type?: 'alerting';
|
||||
state?: State;
|
||||
}
|
||||
|
||||
interface RecordingStateIconProps extends CommonStateIconsProps {
|
||||
type: 'recording';
|
||||
state?: never;
|
||||
}
|
||||
|
||||
type StateIconProps = AlertingStateIconProps | RecordingStateIconProps;
|
||||
|
||||
export type RuleOperation = 'creating' | 'deleting';
|
||||
|
||||
const icons: Record<State, IconName> = {
|
||||
normal: 'check-circle',
|
||||
pending: 'circle',
|
||||
recovering: 'exclamation-circle',
|
||||
firing: 'exclamation-circle',
|
||||
unknown: 'question-circle',
|
||||
};
|
||||
|
||||
const color: Record<State, TextProps['color']> = {
|
||||
normal: 'success',
|
||||
pending: 'warning',
|
||||
recovering: 'warning',
|
||||
firing: 'error',
|
||||
unknown: 'secondary',
|
||||
};
|
||||
|
||||
const stateNames: Record<State, string> = {
|
||||
normal: 'Normal',
|
||||
pending: 'Pending',
|
||||
firing: 'Firing',
|
||||
recovering: 'Recovering',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
|
||||
const operationIcons: Record<RuleOperation, IconName> = {
|
||||
creating: 'plus-circle',
|
||||
deleting: 'minus-circle',
|
||||
};
|
||||
|
||||
// ⚠️ not trivial to update this, you have to re-do the math for the loading spinner
|
||||
const ICON_SIZE = 15;
|
||||
|
||||
/**
|
||||
* Make sure that the order of importance here matches the one we use in the StateBadge component for the detail view
|
||||
* This component is often rendered tens or hundreds of times in a single page, so it's performance is important
|
||||
*
|
||||
* @TODO support translations
|
||||
*/
|
||||
export const StateIcon = memo(function StateIcon({
|
||||
state,
|
||||
health,
|
||||
type = 'alerting',
|
||||
isPaused = false,
|
||||
operation,
|
||||
}: StateIconProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const theme = useTheme2();
|
||||
|
||||
let iconName: IconName = state ? icons[state] : 'circle';
|
||||
let iconColor: TextProps['color'] = state ? color[state] : 'secondary';
|
||||
let stateName: string = state ? stateNames[state] : 'unknown';
|
||||
|
||||
if (type === 'recording') {
|
||||
iconName = 'record-audio';
|
||||
iconColor = 'success';
|
||||
stateName = 'Recording';
|
||||
}
|
||||
|
||||
if (health === 'nodata') {
|
||||
iconName = 'exclamation-triangle';
|
||||
iconColor = 'warning';
|
||||
stateName = 'Insufficient data';
|
||||
}
|
||||
|
||||
if (health === 'error') {
|
||||
iconName = 'times-circle';
|
||||
iconColor = 'error';
|
||||
stateName = 'Failed to evaluate rule';
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
iconName = 'pause-circle';
|
||||
iconColor = 'warning';
|
||||
stateName = 'Paused';
|
||||
}
|
||||
|
||||
if (operation) {
|
||||
iconName = operationIcons[operation];
|
||||
iconColor = 'secondary';
|
||||
stateName = upperFirst(operation);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={stateName} placement="right">
|
||||
<div>
|
||||
<Text color={iconColor}>
|
||||
<div className={styles.iconsContainer}>
|
||||
<Icon name={iconName} width={ICON_SIZE} height={ICON_SIZE} aria-label={stateName} />
|
||||
{/* this loading spinner works by using an optical illusion;
|
||||
the actual icon is static and the "spinning" part is just a semi-transparent darker circle overlayed on top.
|
||||
This makes it look like there is a small bright colored spinner rotating.
|
||||
*/}
|
||||
{operation && (
|
||||
<svg
|
||||
width={ICON_SIZE}
|
||||
height={ICON_SIZE}
|
||||
viewBox="0 0 20 20"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.spinning}
|
||||
>
|
||||
<circle
|
||||
r={ICON_SIZE / 2}
|
||||
cx="10"
|
||||
cy="10"
|
||||
// make sure to match this color to the color of the list item background where it's being used! Works for both light and dark themes.
|
||||
stroke={theme.colors.background.primary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="transparent"
|
||||
strokeOpacity={0.85}
|
||||
strokeDasharray="20px"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const spin = keyframes({
|
||||
'0%': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(360deg)',
|
||||
},
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
iconsContainer: css({
|
||||
position: 'relative',
|
||||
width: ICON_SIZE,
|
||||
height: ICON_SIZE,
|
||||
'> *': {
|
||||
position: 'absolute',
|
||||
},
|
||||
}),
|
||||
spinning: css({
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
animationName: spin,
|
||||
animationIterationCount: 'infinite',
|
||||
animationDuration: '1s',
|
||||
animationTimingFunction: 'linear',
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ArgTypes } from '@storybook/blocks';
|
||||
import { Stack } from '@grafana/ui';
|
||||
import { StateText } from './StateText';
|
||||
|
||||
# StateText
|
||||
|
||||
A component for showing the state and health of a rule. The health of the rule will take precedence over its state.
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import { StateText } from './StateText';
|
||||
import mdx from './StateText.mdx';
|
||||
|
||||
const meta: Meta<typeof StateText> = {
|
||||
component: StateText,
|
||||
title: 'Rules/StateText',
|
||||
decorators: [],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const AlertRule: StoryObj<typeof StateText> = {
|
||||
render: (args: ComponentProps<typeof StateText>) => (
|
||||
<Stack direction="column" alignItems="flex-start">
|
||||
<StateText {...args} />
|
||||
<hr />
|
||||
<StateText type="alerting" state="normal" />
|
||||
<StateText type="alerting" state="pending" />
|
||||
<StateText type="alerting" state="firing" />
|
||||
<StateText type="alerting" state="recovering" />
|
||||
<StateText type="alerting" state="unknown" />
|
||||
<hr />
|
||||
<StateText type="alerting" state="firing" health="error" />
|
||||
<StateText type="alerting" state="firing" health="nodata" />
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
export const RecordingRule: StoryObj<typeof StateText> = {
|
||||
render: (args: ComponentProps<typeof StateText>) => (
|
||||
<Stack direction="column" alignItems="flex-start">
|
||||
<StateText type="recording" health="error" />
|
||||
<StateText type="recording" />
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { render, screen } from '../../../../../tests/test-utils';
|
||||
|
||||
import { StateText } from './StateText';
|
||||
|
||||
describe('StateText', () => {
|
||||
describe('alert type', () => {
|
||||
it('should render the state for "normal"', () => {
|
||||
render(<StateText state="normal" />);
|
||||
expect(screen.getByText('Normal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the state for "firing"', () => {
|
||||
render(<StateText state="firing" />);
|
||||
expect(screen.getByText('Firing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the state for "pending"', () => {
|
||||
render(<StateText state="pending" />);
|
||||
expect(screen.getByText('Pending')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the state for "paused"', () => {
|
||||
render(<StateText isPaused />);
|
||||
expect(screen.getByText('Paused')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Error" when health is "error", even when state is "normal"', () => {
|
||||
render(<StateText state="normal" health="error" />);
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Normal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "No data" when health is "nodata", even when state is "firing"', () => {
|
||||
render(<StateText state="firing" health="nodata" />);
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Firing')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Error" when health is "error", even when state is "pending"', () => {
|
||||
render(<StateText state="pending" health="error" />);
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Pending')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('recording type', () => {
|
||||
it('should render "Recording" for recording rule type', () => {
|
||||
render(<StateText type="recording" />);
|
||||
expect(screen.getByText('Recording')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Recording error" for recording rule type when health is "error"', () => {
|
||||
render(<StateText type="recording" health="error" />);
|
||||
expect(screen.getByText('Recording error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Icon, Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { StateDot } from './StateDot';
|
||||
import { Health, State } from './types';
|
||||
|
||||
// we're making a distinction here between the "state" of the rule and its "health".
|
||||
// When the type is "recording" we only support the health property.
|
||||
type CommonStateTextProps = {
|
||||
health?: Health;
|
||||
isPaused?: boolean;
|
||||
};
|
||||
|
||||
interface AlertingStateTextProps extends CommonStateTextProps {
|
||||
type?: 'alerting';
|
||||
state?: State;
|
||||
}
|
||||
|
||||
interface RecordingStateTextProps extends CommonStateTextProps {
|
||||
type: 'recording';
|
||||
state?: never;
|
||||
}
|
||||
|
||||
type StateTextProps = AlertingStateTextProps | RecordingStateTextProps;
|
||||
|
||||
export const StateText = ({ state, health, type = 'alerting', isPaused = false }: StateTextProps) => {
|
||||
if (isPaused) {
|
||||
return <PausedText />;
|
||||
}
|
||||
|
||||
let stateLabel: string;
|
||||
let color: TextColor;
|
||||
|
||||
switch (state) {
|
||||
case 'normal':
|
||||
color = 'success';
|
||||
stateLabel = 'Normal';
|
||||
break;
|
||||
case 'firing':
|
||||
color = 'error';
|
||||
stateLabel = 'Firing';
|
||||
break;
|
||||
case 'pending':
|
||||
color = 'warning';
|
||||
stateLabel = 'Pending';
|
||||
break;
|
||||
case 'recovering':
|
||||
color = 'warning';
|
||||
stateLabel = 'Recovering';
|
||||
break;
|
||||
case 'unknown':
|
||||
default:
|
||||
color = 'unknown';
|
||||
stateLabel = 'Unknown';
|
||||
break;
|
||||
}
|
||||
|
||||
// if the rule is in "error" health we don't really care about the state
|
||||
if (health === 'error') {
|
||||
color = 'error';
|
||||
stateLabel = 'Error';
|
||||
}
|
||||
|
||||
if (health === 'nodata') {
|
||||
color = 'warning';
|
||||
stateLabel = 'No data';
|
||||
}
|
||||
|
||||
// recording rule badge
|
||||
// @TODO do recording rules support "nodata" state?
|
||||
if (type === 'recording') {
|
||||
const text = health === 'error' ? 'Recording error' : 'Recording';
|
||||
const color = health === 'error' ? 'error' : 'success';
|
||||
|
||||
return <InnerText color={color} text={text} />;
|
||||
}
|
||||
|
||||
return <InnerText color={color} text={stateLabel} />;
|
||||
};
|
||||
|
||||
// the generic badge component
|
||||
type TextColor = 'success' | 'error' | 'warning' | 'unknown';
|
||||
|
||||
interface InnerTextProps {
|
||||
color: TextColor;
|
||||
text: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
// the inner badge component doesn't care about the semantics of "state" or "health" but just renders
|
||||
// a badge in a specific text color and a dot in matching color.
|
||||
// We currently don't expose this component outside of this file.
|
||||
function InnerText({ color, text }: InnerTextProps) {
|
||||
const textColor = color === 'unknown' ? 'secondary' : color;
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} wrap="nowrap" flex="0 0 auto" alignItems="center">
|
||||
<StateDot color={color} />
|
||||
<Text variant="bodySmall" color={textColor}>
|
||||
{text}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function PausedText() {
|
||||
return (
|
||||
<Text variant="bodySmall" color="warning">
|
||||
<Stack direction="row" gap={0.5} wrap="nowrap" flex="0 0 auto" alignItems="center">
|
||||
<Icon name="pause" size="xs" />
|
||||
<Trans i18nKey="alerting.paused-badge.paused">Paused</Trans>
|
||||
</Stack>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type Health = 'nodata' | 'error';
|
||||
export type State = 'normal' | 'firing' | 'pending' | 'unknown' | 'recovering';
|
||||
export type Type = 'alerting' | 'recording';
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alerting": {
|
||||
"paused-badge": {
|
||||
"paused": "Paused"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages
|
||||
sort: true,
|
||||
createOldCatalogs: false,
|
||||
failOnWarnings: true,
|
||||
verbose: false,
|
||||
resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code
|
||||
|
||||
defaultNamespace: 'grafana-alerting',
|
||||
input: ['../**/*.{tsx,ts}'],
|
||||
output: './src/locales/$LOCALE/$NAMESPACE.json',
|
||||
};
|
||||
@@ -25,6 +25,10 @@ export {
|
||||
export { USER_DEFINED_TREE_NAME } from './grafana/notificationPolicies/consts';
|
||||
export * from './grafana/notificationPolicies/types';
|
||||
|
||||
// Rules
|
||||
export { StateText } from './grafana/rules/components/state/StateText';
|
||||
export { StateIcon } from './grafana/rules/components/state/StateIcon';
|
||||
|
||||
// Matchers
|
||||
export { type LabelMatcher, type Label } from './grafana/matchers/types';
|
||||
export { matchLabelsSet, matchLabels, isLabelMatch, type LabelMatchDetails } from './grafana/matchers/utils';
|
||||
|
||||
@@ -2915,6 +2915,7 @@ __metadata:
|
||||
resolution: "@grafana/alerting@workspace:packages/grafana-alerting"
|
||||
dependencies:
|
||||
"@faker-js/faker": "npm:^9.8.0"
|
||||
"@grafana/i18n": "npm:12.2.0-pre"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
"@rtk-query/codegen-openapi": "npm:^2.0.0"
|
||||
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||
@@ -2924,6 +2925,7 @@ __metadata:
|
||||
"@types/react": "npm:18.3.18"
|
||||
"@types/react-dom": "npm:18.3.5"
|
||||
fishery: "npm:^2.3.1"
|
||||
i18next: "npm:^25.5.2"
|
||||
lodash: "npm:^4.17.21"
|
||||
react: "npm:18.3.1"
|
||||
react-dom: "npm:18.3.1"
|
||||
@@ -19226,6 +19228,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next@npm:^25.5.2":
|
||||
version: 25.5.2
|
||||
resolution: "i18next@npm:25.5.2"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.27.6"
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10/8d52e82386722a228f4465aa5cf39d82bd9861ea9cc8b31d7cc4d22d5e70b2740ee006068b09f486d5273cabd227a2ac14f37d68fab26344524200083f679bcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
|
||||
Reference in New Issue
Block a user