Alerting: Central alert state history part3 (#89842)

* Implement EventDetails for expanded rows and pagination on the events list

* Add test for getPanelDataForRule function

* prettier

* refactor EventState component

* create interfaces for props

* Add missing translations

* Update some comments

* prettier and extract translations

* remove unnecessary translations

* update translations

* address review comments

* address review pr comments
This commit is contained in:
Sonia Aguilar 2024-07-04 16:21:39 +02:00 committed by GitHub
parent b174c1310a
commit 7f4a1469e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 518 additions and 96 deletions

View File

@ -140,7 +140,7 @@ interface AnnotationValueProps {
value: string;
}
function AnnotationValue({ value }: AnnotationValueProps) {
export function AnnotationValue({ value }: AnnotationValueProps) {
const needsExternalLink = value && value.startsWith('http');
const tokenizeValue = <Tokenize input={value} delimiter={['{{', '}}']} />;

View File

@ -64,7 +64,12 @@ export const CentralAlertHistoryScene = () => {
new SceneTimePicker({}),
new SceneRefreshPicker({}),
],
$timeRange: new SceneTimeRange({}), //needed for using the time range sync in the url
// use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
// and using a wider time range might lead to showing gaps in the events list and the chart.
$timeRange: new SceneTimeRange({
from: 'now-1h',
to: 'now',
}),
$variables: new SceneVariableSet({
variables: [filterVariable],
}),
@ -170,16 +175,16 @@ export const FilterInfo = () => {
<Tooltip
content={
<div>
<Trans i18nKey="central-alert-history.filter.info.label1">
<Trans i18nKey="alerting.central-alert-history.filter.info.label1">
Filter events using label querying without spaces, ex:
</Trans>
<pre>{`{severity="critical", instance=~"cluster-us-.+"}`}</pre>
<Trans i18nKey="central-alert-history.filter.info.label2">Invalid use of spaces:</Trans>
<pre>{`{severity= "critical"}`}</pre>
<Trans i18nKey="alerting.central-alert-history.filter.info.label2">Invalid use of spaces:</Trans>
<pre>{`{severity= "alerting.critical"}`}</pre>
<pre>{`{severity ="critical"}`}</pre>
<Trans i18nKey="central-alert-history.filter.info.label3">Valid use of spaces:</Trans>
<Trans i18nKey="alerting.central-alert-history.filter.info.label3">Valid use of spaces:</Trans>
<pre>{`{severity=" critical"}`}</pre>
<Trans i18nKey="central-alert-history.filter.info.label4">
<Trans i18nKey="alerting.central-alert-history.filter.info.label4">
Filter alerts using label querying without braces, ex:
</Trans>
<pre>{`severity="critical", instance=~"cluster-us-.+"`}</pre>

View File

@ -0,0 +1,236 @@
import { css } from '@emotion/css';
import { max, min, uniqBy } from 'lodash';
import { useMemo } from 'react';
import { FieldType, GrafanaTheme2, LoadingState, PanelData, dateTime, makeTimeRange } from '@grafana/data';
import { Icon, Stack, Text, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { CombinedRule } from 'app/types/unified-alerting';
import { useCombinedRule } from '../../../hooks/useCombinedRule';
import { parse } from '../../../utils/rule-id';
import { isGrafanaRulerRule } from '../../../utils/rules';
import { MetaText } from '../../MetaText';
import { VizWrapper } from '../../rule-editor/VizWrapper';
import { AnnotationValue } from '../../rule-viewer/tabs/Details';
import { LogRecord } from '../state-history/common';
import { EventState } from './EventListSceneObject';
interface EventDetailsProps {
record: LogRecord;
logRecords: LogRecord[];
}
export function EventDetails({ record, logRecords }: EventDetailsProps) {
// get the rule from the ruleUID
const ruleUID = record.line?.ruleUID ?? '';
const identifier = useMemo(() => {
return parse(ruleUID, true);
}, [ruleUID]);
const { error, loading, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
if (error) {
return (
<Text>
<Trans i18nKey="alerting.central-alert-history.details.error">Error loading rule for this event.</Trans>
</Text>
);
}
if (loading) {
return (
<Text>
<Trans i18nKey="alerting.central-alert-history.details.loading">Loading...</Trans>
</Text>
);
}
if (!rule) {
return (
<Text>
<Trans i18nKey="alerting.central-alert-history.details.not-found">Rule not found for this event.</Trans>
</Text>
);
}
const getTransitionsCountByRuleUID = (ruleUID: string) => {
return logRecords.filter((record) => record.line.ruleUID === ruleUID).length;
};
return (
<Stack direction="column" gap={0.5}>
<Stack direction={'row'} gap={6}>
<StateTransition record={record} />
<ValueInTransition record={record} />
<NumberTransitions transitions={ruleUID ? getTransitionsCountByRuleUID(ruleUID) : 0} />
</Stack>
<Annotations rule={rule} />
<QueryVizualization rule={rule} ruleUID={ruleUID} logRecords={logRecords} />
</Stack>
);
}
interface StateTransitionProps {
record: LogRecord;
}
function StateTransition({ record }: StateTransitionProps) {
return (
<Stack gap={0.5} direction={'column'}>
<Text variant="body" weight="light" color="secondary">
<Trans i18nKey="alerting.central-alert-history.details.state-transitions">State transition</Trans>
</Text>
<Stack gap={0.5} direction={'row'} alignItems="center">
<EventState state={record.line.previous} showLabel />
<Icon name="arrow-right" size="lg" />
<EventState state={record.line.current} showLabel />
</Stack>
</Stack>
);
}
interface AnnotationsProps {
rule: CombinedRule;
}
const Annotations = ({ rule }: AnnotationsProps) => {
const styles = useStyles2(getStyles);
const annotations = rule.annotations;
if (!annotations) {
return null;
}
return (
<>
<Text variant="body" color="secondary" weight="light">
<Trans i18nKey="alerting.central-alert-history.details.annotations">Annotations</Trans>
</Text>
{Object.keys(annotations).length === 0 ? (
<Text variant="body" weight="light" italic>
<Trans i18nKey="alerting.central-alert-history.details.no-annotations">No annotations</Trans>
</Text>
) : (
<div className={styles.metadataWrapper}>
{Object.entries(annotations).map(([name, value]) => (
<MetaText direction="column" key={name}>
{name}
<AnnotationValue value={value} />
</MetaText>
))}
</div>
)}
</>
);
};
/**
*
* This component renders the visualization for the rule condition values over the selected time range.
* The visualization is a time series graph with the condition values on the y-axis and time on the x-axis.
* The values are extracted from the log records already fetched from the history api.
* The graph is rendered only if the rule is a Grafana rule.
*
*/
interface QueryVizualizationProps {
ruleUID: string;
rule: CombinedRule;
logRecords: LogRecord[];
}
const QueryVizualization = ({ ruleUID, rule, logRecords }: QueryVizualizationProps) => {
if (!isGrafanaRulerRule(rule?.rulerRule)) {
return (
<Text>
<Trans i18nKey="alerting.central-alert-history.details.not-grafana-rule">Rule is not a Grafana rule</Trans>
</Text>
);
}
// get the condition from the rule
const condition = rule?.rulerRule.grafana_alert?.condition ?? 'A';
// get the panel data for the rule
const panelData = getPanelDataForRule(ruleUID, logRecords, condition);
// render the visualization
return <VizWrapper data={panelData} thresholds={undefined} thresholdsType={undefined} />;
};
/**
* This function returns the time series panel data for the condtion values of the rule, within the selected time range.
* The values are extracted from the log records already fetched from the history api.
* @param ruleUID
* @param logRecords
* @param condition
* @returns PanelData
*/
export function getPanelDataForRule(ruleUID: string, logRecords: LogRecord[], condition: string) {
const ruleLogRecords = logRecords
.filter((record) => record.line.ruleUID === ruleUID)
// sort by timestamp as time series data is expected to be sorted by time
.sort((a, b) => a.timestamp - b.timestamp);
// get unique records by timestamp, as timeseries data should have unique timestamps, and it might be possible to have multiple records with the same timestamp
const uniqueRecords = uniqBy(ruleLogRecords, (record) => record.timestamp);
const timestamps = uniqueRecords.map((record) => record.timestamp);
const values = uniqueRecords.map((record) => (record.line.values ? record.line.values[condition] : 0));
const minTimestamp = min(timestamps);
const maxTimestamp = max(timestamps);
const PanelDataObj: PanelData = {
series: [
{
name: 'Rule condition history',
fields: [
{ name: 'Time', values: timestamps, config: {}, type: FieldType.time },
{ name: 'values', values: values, type: FieldType.number, config: {} },
],
length: timestamps.length,
},
],
state: LoadingState.Done,
timeRange: makeTimeRange(dateTime(minTimestamp), dateTime(maxTimestamp)),
};
return PanelDataObj;
}
interface ValueInTransitionProps {
record: LogRecord;
}
function ValueInTransition({ record }: ValueInTransitionProps) {
const values = record?.line?.values
? JSON.stringify(record.line.values)
: t('alerting.central-alert-history.details.no-values', 'No values');
return (
<Stack gap={0.5} direction={'column'}>
<Text variant="body" weight="light" color="secondary">
<Trans i18nKey="alerting.central-alert-history.details.value-in-transition">Value in transition</Trans>
</Text>
<Stack gap={0.5} direction={'row'} alignItems="center">
<Text variant="body" weight="light">
{values}
</Text>
</Stack>
</Stack>
);
}
interface NumberTransitionsProps {
transitions: number;
}
function NumberTransitions({ transitions }: NumberTransitionsProps) {
return (
<Stack gap={0.5} direction={'column'} alignItems="flex-start" justifyContent={'center'}>
<Text variant="body" weight="light" color="secondary">
<Trans i18nKey="alerting.central-alert-history.details.number-transitions">
State transitions for selected period
</Trans>
</Text>
<Text variant="body" weight="light">
{transitions}
</Text>
</Stack>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
metadataWrapper: css({
display: 'grid',
gridTemplateColumns: 'auto auto',
rowGap: theme.spacing(3),
columnGap: theme.spacing(12),
}),
};
};

View File

@ -1,13 +1,13 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react';
import { css, cx } from '@emotion/css';
import { ReactElement, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DataFrameJSON, GrafanaTheme2, TimeRange } from '@grafana/data';
import { DataFrameJSON, GrafanaTheme2, IconName, TimeRange } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, TextBoxVariable, VariableValue, sceneGraph } from '@grafana/scenes';
import { Alert, Icon, LoadingBar, Stack, Text, Tooltip, useStyles2, withErrorBoundary } from '@grafana/ui';
import { Alert, Icon, LoadingBar, Pagination, Stack, Text, Tooltip, useStyles2, withErrorBoundary } from '@grafana/ui';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { t } from 'app/core/internationalization';
import { Trans, t } from 'app/core/internationalization';
import {
GrafanaAlertStateWithReason,
isAlertStateWithReason,
@ -17,6 +17,7 @@ import {
} from 'app/types/unified-alerting-dto';
import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { usePagination } from '../../../hooks/usePagination';
import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { stringifyErrorLike } from '../../../utils/misc';
@ -26,8 +27,10 @@ import { LogRecord } from '../state-history/common';
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
import { LABELS_FILTER } from './CentralAlertHistoryScene';
import { EventDetails } from './EventDetails';
export const LIMIT_EVENTS = 5000; // limit is hard-capped at 5000 at the BE level.
const PAGE_SIZE = 100;
/**
*
@ -35,13 +38,11 @@ export const LIMIT_EVENTS = 5000; // limit is hard-capped at 5000 at the BE leve
* It fetches the events from the history api and displays them in a list.
* The list is filtered by the labels in the filter variable and by the time range variable in the scene graph.
*/
export const HistoryEventsList = ({
timeRange,
valueInfilterTextBox,
}: {
interface HistoryEventsListProps {
timeRange?: TimeRange;
valueInfilterTextBox: VariableValue;
}) => {
}
export const HistoryEventsList = ({ timeRange, valueInfilterTextBox }: HistoryEventsListProps) => {
const from = timeRange?.from.unix();
const to = timeRange?.to.unix();
@ -85,12 +86,23 @@ interface HistoryLogEventsProps {
logRecords: LogRecord[];
}
function HistoryLogEvents({ logRecords }: HistoryLogEventsProps) {
const { page, pageItems, numberOfPages, onPageChange } = usePagination(logRecords, 1, PAGE_SIZE);
return (
<ul>
{logRecords.map((record) => {
return <EventRow key={record.timestamp + (record.line.fingerprint ?? '')} record={record} />;
})}
</ul>
<Stack direction="column" gap={0}>
<ul>
{pageItems.map((record) => {
return (
<EventRow
key={record.timestamp + (record.line.fingerprint ?? '')}
record={record}
logRecords={logRecords}
/>
);
})}
</ul>
{/* This paginations improves the performance considerably , making the page load faster */}
<Pagination currentPage={page} numberOfPages={numberOfPages} onNavigate={onPageChange} hideWhenSinglePage />
</Stack>
);
}
@ -102,17 +114,25 @@ function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
if (isFetchError(error) && error.status === 404) {
return <EntityNotFound entity="History" />;
}
const title = t('central-alert-history.error', 'Something went wrong loading the alert state history');
const title = t('alerting.central-alert-history.error', 'Something went wrong loading the alert state history');
const errorStr = stringifyErrorLike(error);
return <Alert title={title}>{stringifyErrorLike(error)}</Alert>;
return <Alert title={title}>{errorStr}</Alert>;
}
function EventRow({ record }: { record: LogRecord }) {
interface EventRowProps {
record: LogRecord;
logRecords: LogRecord[];
}
function EventRow({ record, logRecords }: EventRowProps) {
const styles = useStyles2(getStyles);
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div>
<div className={styles.header} data-testid="event-row-header">
<Stack direction="column" gap={0}>
<div
className={cx(styles.header, isCollapsed ? styles.collapsedHeader : styles.notCollapsedHeader)}
data-testid="event-row-header"
>
<CollapseToggle
size="sm"
className={styles.collapseToggle}
@ -134,15 +154,29 @@ function EventRow({ record }: { record: LogRecord }) {
</div>
</Stack>
</div>
</div>
{!isCollapsed && (
<div className={styles.expandedRow}>
<EventDetails record={record} logRecords={logRecords} />
</div>
)}
</Stack>
);
}
function AlertRuleName({ labels, ruleUID }: { labels: Record<string, string>; ruleUID?: string }) {
interface AlertRuleNameProps {
labels: Record<string, string>;
ruleUID?: string;
}
function AlertRuleName({ labels, ruleUID }: AlertRuleNameProps) {
const styles = useStyles2(getStyles);
const alertRuleName = labels['alertname'];
if (!ruleUID) {
return <Text>{alertRuleName}</Text>;
return (
<Text>
<Trans i18nKey="alerting.central-alert-history.details.unknown-rule">Unknown</Trans>
{alertRuleName}
</Text>
);
}
return (
<Tooltip content={alertRuleName ?? ''}>
@ -170,55 +204,90 @@ function EventTransition({ previous, current }: EventTransitionProps) {
);
}
function EventState({ state }: { state: GrafanaAlertStateWithReason }) {
const styles = useStyles2(getStyles);
interface StateIconProps {
iconName: IconName;
iconColor: string;
tooltipContent: string;
labelText: ReactElement;
showLabel: boolean;
}
const StateIcon = ({ iconName, iconColor, tooltipContent, labelText, showLabel }: StateIconProps) => (
<Tooltip content={tooltipContent}>
<Stack gap={0.5} direction={'row'} alignItems="center">
<Icon name={iconName} size="md" className={iconColor} />
{showLabel && (
<Text variant="body" weight="light">
{labelText}
</Text>
)}
</Stack>
</Tooltip>
);
interface EventStateProps {
state: GrafanaAlertStateWithReason;
showLabel?: boolean;
}
export function EventState({ state, showLabel = false }: EventStateProps) {
const styles = useStyles2(getStyles);
const toolTip = t('alerting.central-alert-history.details.no-recognized-state', 'No recognized state');
if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) {
return (
<Tooltip content={'No recognized state'}>
<Icon name="exclamation-triangle" size="md" />
</Tooltip>
<StateIcon
iconName="exclamation-triangle"
tooltipContent={toolTip}
labelText={<Trans i18nKey="alerting.central-alert-history.details.unknown-event-state">Unknown</Trans>}
showLabel={Boolean(showLabel)}
iconColor={styles.warningColor}
/>
);
}
const baseState = mapStateWithReasonToBaseState(state);
const reason = mapStateWithReasonToReason(state);
switch (baseState) {
case 'Normal':
return (
<Tooltip content={Boolean(reason) ? `Normal (${reason})` : 'Normal'}>
<Icon name="check-circle" size="md" className={Boolean(reason) ? styles.warningColor : styles.normalColor} />
</Tooltip>
);
case 'Alerting':
return (
<Tooltip content={'Alerting'}>
<Icon name="exclamation-circle" size="md" className={styles.alertingColor} />
</Tooltip>
);
case 'NoData': //todo:change icon
return (
<Tooltip content={'Insufficient data'}>
<Icon name="exclamation-triangle" size="md" className={styles.warningColor} />
{/* no idea which icon to use */}
</Tooltip>
);
case 'Error':
return (
<Tooltip content={'Error'}>
<Icon name="exclamation-circle" size="md" />
</Tooltip>
);
case 'Pending':
return (
<Tooltip content={Boolean(reason) ? `Pending (${reason})` : 'Pending'}>
<Icon name="circle" size="md" className={styles.warningColor} />
</Tooltip>
);
default:
return <Icon name="exclamation-triangle" size="md" />;
interface StateConfig {
iconName: IconName;
iconColor: string;
tooltipContent: string;
labelText: ReactElement;
}
interface StateConfigMap {
[key: string]: StateConfig;
}
const stateConfig: StateConfigMap = {
Normal: {
iconName: 'check-circle',
iconColor: Boolean(reason) ? styles.warningColor : styles.normalColor,
tooltipContent: Boolean(reason) ? `Normal (${reason})` : 'Normal',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.normal">Normal</Trans>,
},
Alerting: {
iconName: 'exclamation-circle',
iconColor: styles.alertingColor,
tooltipContent: 'Alerting',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.alerting">Alerting</Trans>,
},
NoData: {
iconName: 'exclamation-triangle',
iconColor: styles.warningColor,
tooltipContent: 'Insufficient data',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.no-data">No data</Trans>,
},
Error: {
iconName: 'exclamation-circle',
tooltipContent: 'Error',
iconColor: styles.warningColor,
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.error">Error</Trans>,
},
Pending: {
iconName: 'circle',
iconColor: styles.warningColor,
tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending',
labelText: <Trans i18nKey="alerting.central-alert-history.details.state.pending">Pending</Trans>,
},
};
const config = stateConfig[baseState] || { iconName: 'exclamation-triangle', tooltipContent: 'Unknown State' };
return <StateIcon {...config} showLabel={showLabel} />;
}
interface TimestampProps {
@ -253,12 +322,16 @@ export const getStyles = (theme: GrafanaTheme2) => {
alignItems: 'center',
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
flexWrap: 'nowrap',
borderBottom: `1px solid ${theme.colors.border.weak}`,
'&:hover': {
backgroundColor: theme.components.table.rowHoverBackground,
},
}),
collapsedHeader: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
notCollapsedHeader: css({
borderBottom: 'none',
}),
collapseToggle: css({
background: 'none',
@ -303,6 +376,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
display: 'block',
color: theme.colors.text.link,
}),
expandedRow: css({
padding: theme.spacing(2),
marginLeft: theme.spacing(2),
borderLeft: `1px solid ${theme.colors.border.weak}`,
}),
};
};

View File

@ -0,0 +1,53 @@
import { dateTime } from '@grafana/data';
import { LogRecord } from '../state-history/common';
import { getPanelDataForRule } from './EventDetails';
const initialTimeStamp = 1000000;
const instanceLabels = { foo: 'bar', severity: 'critical', cluster: 'dev-us' }; // actually, it doesn't matter what is here
const records: LogRecord[] = [
{
timestamp: initialTimeStamp,
line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 1 } },
},
{
timestamp: initialTimeStamp + 1000,
line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID2' },
},
{
timestamp: initialTimeStamp + 2000,
line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID3' },
},
// not sorted by timestamp
{
timestamp: initialTimeStamp + 4000,
line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 8 } },
},
{
timestamp: initialTimeStamp + 3000,
line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
},
//duplicate record in the same timestamp
{
timestamp: initialTimeStamp + 3000,
line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
},
{
timestamp: initialTimeStamp + 5000,
line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
},
];
describe('getPanelDataForRule', () => {
it('should return correct panel data for a given rule (sorted by time and unique)', () => {
const result = getPanelDataForRule('ruleUID1', records, 'C');
expect(result.series[0].fields[0].values).toEqual([1000000, 1003000, 1004000, 1005000]);
expect(result.series[0].fields[1].values).toEqual([1, 0, 8, 0]);
expect(result.series[0].fields[0].type).toEqual('time');
expect(result.series[0].fields[1].type).toEqual('number');
expect(result.state).toEqual('Done');
expect(result.timeRange.from).toEqual(dateTime(1000000));
expect(result.timeRange.to).toEqual(dateTime(1005000));
});
});

View File

@ -47,9 +47,15 @@ export function mapStateWithReasonToReason(state: GrafanaAlertStateWithReason):
return match ? match[1] : '';
}
type StateWithReasonToBaseStateReturnType<T> = T extends GrafanaAlertStateWithReason
? GrafanaAlertState
: T extends PromAlertingRuleState
? PromAlertingRuleState
: never;
export function mapStateWithReasonToBaseState(
state: GrafanaAlertStateWithReason | PromAlertingRuleState
): GrafanaAlertState | PromAlertingRuleState {
): StateWithReasonToBaseStateReturnType<GrafanaAlertStateWithReason | PromAlertingRuleState> {
if (isAlertStateWithReason(state)) {
const fields = state.split(' ');
return fields[0] as GrafanaAlertState;

View File

@ -56,6 +56,39 @@
"pause": "Pause evaluation"
},
"alerting": {
"central-alert-history": {
"details": {
"annotations": "Annotations",
"error": "Error loading rule for this event.",
"loading": "Loading...",
"no-annotations": "No annotations",
"no-recognized-state": "No recognized state",
"no-values": "No values",
"not-found": "Rule not found for this event.",
"not-grafana-rule": "Rule is not a Grafana rule",
"number-transitions": "State transitions for selected period",
"state": {
"alerting": "Alerting",
"error": "Error",
"no-data": "No data",
"normal": "Normal",
"pending": "Pending"
},
"state-transitions": "State transition",
"unknown-event-state": "Unknown",
"unknown-rule": "Unknown",
"value-in-transition": "Value in transition"
},
"error": "Something went wrong loading the alert state history",
"filter": {
"info": {
"label1": "Filter events using label querying without spaces, ex:",
"label2": "Invalid use of spaces:",
"label3": "Valid use of spaces:",
"label4": "Filter alerts using label querying without braces, ex:"
}
}
},
"contact-points": {
"telegram": {
"parse-mode-warning-body": "If you use a <1>parse_mode</1> option other than <3>None</3>, truncation may result in an invalid message, causing the notification to fail. For longer messages, we recommend using an alternative contact method.",
@ -173,17 +206,6 @@
"text": "No results found for your query"
}
},
"central-alert-history": {
"error": "Something went wrong loading the alert state history",
"filter": {
"info": {
"label1": "Filter events using label querying without spaces, ex:",
"label2": "Invalid use of spaces:",
"label3": "Valid use of spaces:",
"label4": "Filter alerts using label querying without braces, ex:"
}
}
},
"clipboard-button": {
"inline-toast": {
"success": "Copied"

View File

@ -56,6 +56,39 @@
"pause": "Päūşę ęväľūäŧįőʼn"
},
"alerting": {
"central-alert-history": {
"details": {
"annotations": "Åʼnʼnőŧäŧįőʼnş",
"error": "Ēřřőř ľőäđįʼnģ řūľę ƒőř ŧĥįş ęvęʼnŧ.",
"loading": "Ŀőäđįʼnģ...",
"no-annotations": "Ńő äʼnʼnőŧäŧįőʼnş",
"no-recognized-state": "Ńő řęčőģʼnįžęđ şŧäŧę",
"no-values": "Ńő väľūęş",
"not-found": "Ŗūľę ʼnőŧ ƒőūʼnđ ƒőř ŧĥįş ęvęʼnŧ.",
"not-grafana-rule": "Ŗūľę įş ʼnőŧ ä Ğřäƒäʼnä řūľę",
"number-transitions": "Ŝŧäŧę ŧřäʼnşįŧįőʼnş ƒőř şęľęčŧęđ pęřįőđ",
"state": {
"alerting": "Åľęřŧįʼnģ",
"error": "Ēřřőř",
"no-data": "Ńő đäŧä",
"normal": "Ńőřmäľ",
"pending": "Pęʼnđįʼnģ"
},
"state-transitions": "Ŝŧäŧę ŧřäʼnşįŧįőʼn",
"unknown-event-state": "Ůʼnĸʼnőŵʼn",
"unknown-rule": "Ůʼnĸʼnőŵʼn",
"value-in-transition": "Väľūę įʼn ŧřäʼnşįŧįőʼn"
},
"error": "Ŝőmęŧĥįʼnģ ŵęʼnŧ ŵřőʼnģ ľőäđįʼnģ ŧĥę äľęřŧ şŧäŧę ĥįşŧőřy",
"filter": {
"info": {
"label1": "Fįľŧęř ęvęʼnŧş ūşįʼnģ ľäþęľ qūęřyįʼnģ ŵįŧĥőūŧ şpäčęş, ęχ:",
"label2": "Ĩʼnväľįđ ūşę őƒ şpäčęş:",
"label3": "Väľįđ ūşę őƒ şpäčęş:",
"label4": "Fįľŧęř äľęřŧş ūşįʼnģ ľäþęľ qūęřyįʼnģ ŵįŧĥőūŧ þřäčęş, ęχ:"
}
}
},
"contact-points": {
"telegram": {
"parse-mode-warning-body": "Ĩƒ yőū ūşę ä <1>päřşę_mőđę</1> őpŧįőʼn őŧĥęř ŧĥäʼn <3>Ńőʼnę</3>, ŧřūʼnčäŧįőʼn mäy řęşūľŧ įʼn äʼn įʼnväľįđ męşşäģę, čäūşįʼnģ ŧĥę ʼnőŧįƒįčäŧįőʼn ŧő ƒäįľ. Főř ľőʼnģęř męşşäģęş, ŵę řęčőmmęʼnđ ūşįʼnģ äʼn äľŧęřʼnäŧįvę čőʼnŧäčŧ męŧĥőđ.",
@ -173,17 +206,6 @@
"text": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy"
}
},
"central-alert-history": {
"error": "Ŝőmęŧĥįʼnģ ŵęʼnŧ ŵřőʼnģ ľőäđįʼnģ ŧĥę äľęřŧ şŧäŧę ĥįşŧőřy",
"filter": {
"info": {
"label1": "Fįľŧęř ęvęʼnŧş ūşįʼnģ ľäþęľ qūęřyįʼnģ ŵįŧĥőūŧ şpäčęş, ęχ:",
"label2": "Ĩʼnväľįđ ūşę őƒ şpäčęş:",
"label3": "Väľįđ ūşę őƒ şpäčęş:",
"label4": "Fįľŧęř äľęřŧş ūşįʼnģ ľäþęľ qūęřyįʼnģ ŵįŧĥőūŧ þřäčęş, ęχ:"
}
}
},
"clipboard-button": {
"inline-toast": {
"success": "Cőpįęđ"