({
query: ({ ruleUid, from, to, limit = 100 }) => ({
url: '/api/v1/rules/history',
params: { ruleUID: ruleUid, from, to, limit },
diff --git a/public/app/features/alerting/unified/components/AlertLabels.tsx b/public/app/features/alerting/unified/components/AlertLabels.tsx
index 1e01be64cf4..40801357372 100644
--- a/public/app/features/alerting/unified/components/AlertLabels.tsx
+++ b/public/app/features/alerting/unified/components/AlertLabels.tsx
@@ -5,6 +5,7 @@ import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
import { isPrivateLabel } from '../utils/labels';
@@ -28,6 +29,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
const commonLabelsCount = Object.keys(commonLabels).length;
const hasCommonLabels = commonLabelsCount > 0;
+ const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
return (
@@ -39,7 +41,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
variant="secondary"
fill="text"
onClick={() => setShowCommonLabels(true)}
- tooltip="Show common labels"
+ tooltip={tooltip}
tooltipPlacement="top"
size="sm"
>
@@ -54,7 +56,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
tooltipPlacement="top"
size="sm"
>
- Hide common labels
+ Hide common labels
)}
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx
new file mode 100644
index 00000000000..e01bb4f6a02
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx
@@ -0,0 +1,383 @@
+import { css } from '@emotion/css';
+import React, { useCallback, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useMeasure } from 'react-use';
+
+import { GrafanaTheme2, TimeRange } from '@grafana/data';
+import { isFetchError } from '@grafana/runtime';
+import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes';
+import {
+ Alert,
+ Button,
+ Field,
+ Icon,
+ Input,
+ Label,
+ LoadingBar,
+ Stack,
+ Text,
+ Tooltip,
+ useStyles2,
+ withErrorBoundary,
+} from '@grafana/ui';
+import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
+import { Trans, t } from 'app/core/internationalization';
+import {
+ GrafanaAlertStateWithReason,
+ isAlertStateWithReason,
+ isGrafanaAlertState,
+ mapStateWithReasonToBaseState,
+ mapStateWithReasonToReason,
+} from 'app/types/unified-alerting-dto';
+
+import { stateHistoryApi } from '../../../api/stateHistoryApi';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { hashLabelsOrAnnotations } from '../../../utils/rule-id';
+import { AlertLabels } from '../../AlertLabels';
+import { CollapseToggle } from '../../CollapseToggle';
+import { LogRecord } from '../state-history/common';
+import { useRuleHistoryRecords } from '../state-history/useRuleHistoryRecords';
+
+const LIMIT_EVENTS = 250;
+
+const HistoryEventsList = ({ timeRange }: { timeRange?: TimeRange }) => {
+ const styles = useStyles2(getStyles);
+
+ // Filter state
+ const [eventsFilter, setEventsFilter] = useState('');
+ // form for filter fields
+ const { register, handleSubmit, reset } = useForm({ defaultValues: { query: '' } }); // form for search field
+ const from = timeRange?.from.unix();
+ const to = timeRange?.to.unix();
+ const onFilterCleared = useCallback(() => {
+ setEventsFilter('');
+ reset();
+ }, [setEventsFilter, reset]);
+
+ const {
+ data: stateHistory,
+ isLoading,
+ isError,
+ error,
+ } = stateHistoryApi.endpoints.getRuleHistory.useQuery(
+ {
+ from: from,
+ to: to,
+ limit: LIMIT_EVENTS,
+ },
+ {
+ refetchOnFocus: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const { historyRecords } = useRuleHistoryRecords(stateHistory, eventsFilter);
+
+ if (isError) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+// todo: this function has been copied from RuleList.v2.tsx, should be moved to a shared location
+const LoadingIndicator = ({ visible = false }) => {
+ const [measureRef, { width }] = useMeasure();
+ return {visible && }
;
+};
+
+interface HistoryLogEventsProps {
+ logRecords: LogRecord[];
+}
+function HistoryLogEvents({ logRecords }: HistoryLogEventsProps) {
+ // display log records
+ return (
+
+ {logRecords.map((record) => {
+ return ;
+ })}
+
+ );
+}
+
+interface HistoryErrorMessageProps {
+ error: unknown;
+}
+
+function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
+ if (isFetchError(error) && error.status === 404) {
+ return ;
+ }
+ const title = t('central-alert-history.error', 'Something went wrong loading the alert state history');
+
+ return {stringifyErrorLike(error)} ;
+}
+
+interface SearchFieldInputProps {
+ showClearFilterSuffix: boolean;
+ onClearFilterClick: () => void;
+}
+const SearchFieldInput = React.forwardRef(
+ ({ showClearFilterSuffix, onClearFilterClick, ...rest }: SearchFieldInputProps, ref) => {
+ const placeholder = t('central-alert-history.filter.placeholder', 'Filter events in the list with labels');
+ return (
+
+
+
+ Filter events
+
+
+
+ }
+ >
+ }
+ suffix={
+ showClearFilterSuffix && (
+
+ Clear
+
+ )
+ }
+ placeholder={placeholder}
+ ref={ref}
+ {...rest}
+ />
+
+ );
+ }
+);
+
+SearchFieldInput.displayName = 'SearchFieldInput';
+
+function EventRow({ record }: { record: LogRecord }) {
+ const styles = useStyles2(getStyles);
+ const [isCollapsed, setIsCollapsed] = useState(true);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {record.line.labels ?
: null}
+
+
+
+
+
+ );
+}
+
+function AlertRuleName({ labels, ruleUID }: { labels: Record; ruleUID?: string }) {
+ const styles = useStyles2(getStyles);
+ const alertRuleName = labels['alertname'];
+ if (!ruleUID) {
+ return {alertRuleName} ;
+ }
+ return (
+
+
+ {alertRuleName}
+
+
+ );
+}
+
+interface EventTransitionProps {
+ previous: GrafanaAlertStateWithReason;
+ current: GrafanaAlertStateWithReason;
+}
+function EventTransition({ previous, current }: EventTransitionProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+function EventState({ state }: { state: GrafanaAlertStateWithReason }) {
+ const styles = useStyles2(getStyles);
+
+ if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) {
+ return (
+
+
+
+ );
+ }
+ const baseState = mapStateWithReasonToBaseState(state);
+ const reason = mapStateWithReasonToReason(state);
+
+ switch (baseState) {
+ case 'Normal':
+ return (
+
+
+
+ );
+ case 'Alerting':
+ return (
+
+
+
+ );
+ case 'NoData': //todo:change icon
+ return (
+
+
+ {/* no idea which icon to use */}
+
+ );
+ case 'Error':
+ return (
+
+
+
+ );
+
+ case 'Pending':
+ return (
+
+
+
+ );
+ default:
+ return ;
+ }
+}
+
+interface TimestampProps {
+ time: number; // epoch timestamp
+}
+
+const Timestamp = ({ time }: TimestampProps) => {
+ const dateTime = new Date(time);
+ const formattedDate = dateTime.toLocaleString('en-US', {
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ });
+
+ return (
+
+ {formattedDate}
+
+ );
+};
+
+export default withErrorBoundary(HistoryEventsList, { style: 'page' });
+
+export const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ header: css({
+ display: 'flex',
+ flexDirection: 'row',
+ 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,
+ },
+ }),
+
+ collapseToggle: css({
+ background: 'none',
+ border: 'none',
+ marginTop: `-${theme.spacing(1)}`,
+ marginBottom: `-${theme.spacing(1)}`,
+
+ svg: {
+ marginBottom: 0,
+ },
+ }),
+ normalColor: css({
+ fill: theme.colors.success.text,
+ }),
+ warningColor: css({
+ fill: theme.colors.warning.text,
+ }),
+ alertingColor: css({
+ fill: theme.colors.error.text,
+ }),
+ timeCol: css({
+ width: '150px',
+ }),
+ transitionCol: css({
+ width: '80px',
+ }),
+ alertNameCol: css({
+ width: '300px',
+ }),
+ labelsCol: css({
+ display: 'flex',
+ overflow: 'hidden',
+ alignItems: 'center',
+ paddingRight: theme.spacing(2),
+ flex: 1,
+ }),
+ alertName: css({
+ whiteSpace: 'nowrap',
+ cursor: 'pointer',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: 'block',
+ color: theme.colors.text.link,
+ }),
+ labelsFilter: css({
+ width: '100%',
+ paddingTop: theme.spacing(4),
+ }),
+ };
+};
+
+export class HistoryEventsListObject extends SceneObjectBase {
+ public static Component = HistoryEventsListObjectRenderer;
+}
+
+export function HistoryEventsListObjectRenderer({ model }: SceneComponentProps) {
+ const { value: timeRange } = sceneGraph.getTimeRange(model).useState(); // get time range from scene graph
+
+ return ;
+}
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx
new file mode 100644
index 00000000000..0ab57f508d4
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { withErrorBoundary } from '@grafana/ui';
+
+import { AlertingPageWrapper } from '../../AlertingPageWrapper';
+
+import { CentralAlertHistoryScene } from './CentralAlertHistoryScene';
+
+const HistoryPage = () => {
+ return (
+
+
+
+ );
+};
+export default withErrorBoundary(HistoryPage, { style: 'page' });
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
new file mode 100644
index 00000000000..0cd75c05a4b
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+
+import { getDataSourceSrv } from '@grafana/runtime';
+import {
+ EmbeddedScene,
+ PanelBuilders,
+ SceneControlsSpacer,
+ SceneFlexItem,
+ SceneFlexLayout,
+ SceneQueryRunner,
+ SceneReactObject,
+ SceneRefreshPicker,
+ SceneTimePicker,
+} from '@grafana/scenes';
+import {
+ GraphDrawStyle,
+ GraphGradientMode,
+ LegendDisplayMode,
+ LineInterpolation,
+ ScaleDistribution,
+ StackingMode,
+ TooltipDisplayMode,
+ VisibilityMode,
+} from '@grafana/schema/dist/esm/index';
+
+import { DataSourceInformation, PANEL_STYLES } from '../../../home/Insights';
+import { SectionSubheader } from '../../../insights/SectionSubheader';
+
+import { HistoryEventsListObjectRenderer } from './CentralAlertHistory';
+
+export const CentralAlertHistoryScene = () => {
+ const dataSourceSrv = getDataSourceSrv();
+ const alertStateHistoryDatasource: DataSourceInformation = {
+ type: 'loki',
+ uid: 'grafanacloud-alert-state-history',
+ settings: undefined,
+ };
+
+ alertStateHistoryDatasource.settings = dataSourceSrv.getInstanceSettings(alertStateHistoryDatasource.uid);
+
+ const scene = new EmbeddedScene({
+ controls: [new SceneControlsSpacer(), new SceneTimePicker({}), new SceneRefreshPicker({})],
+ body: new SceneFlexLayout({
+ direction: 'column',
+ children: [
+ new SceneFlexItem({
+ ySizing: 'content',
+ body: getEventsSceneObject(alertStateHistoryDatasource),
+ }),
+ new SceneFlexItem({
+ body: new SceneReactObject({
+ component: HistoryEventsListObjectRenderer,
+ }),
+ }),
+ ],
+ }),
+ });
+
+ return ;
+};
+
+function getEventsSceneObject(ashDs: DataSourceInformation) {
+ return new EmbeddedScene({
+ controls: [
+ new SceneReactObject({
+ component: SectionSubheader,
+ }),
+ ],
+ body: new SceneFlexLayout({
+ direction: 'column',
+ children: [
+ new SceneFlexItem({
+ ySizing: 'content',
+ body: new SceneFlexLayout({
+ children: [getEventsScenesFlexItem(ashDs)],
+ }),
+ }),
+ ],
+ }),
+ });
+}
+
+function getSceneQuery(datasource: DataSourceInformation) {
+ const query = new SceneQueryRunner({
+ datasource,
+ queries: [
+ {
+ refId: 'A',
+ expr: 'count_over_time({from="state-history"} |= `` [$__auto])',
+ queryType: 'range',
+ step: '10s',
+ },
+ ],
+ });
+ return query;
+}
+
+export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
+ return new SceneFlexItem({
+ ...PANEL_STYLES,
+ body: PanelBuilders.timeseries()
+ .setTitle('Events')
+ .setDescription('Alert events during the period of time.')
+ .setData(getSceneQuery(datasource))
+ .setColor({ mode: 'continuous-BlPu' })
+ .setCustomFieldConfig('fillOpacity', 100)
+ .setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
+ .setCustomFieldConfig('lineInterpolation', LineInterpolation.Linear)
+ .setCustomFieldConfig('lineWidth', 1)
+ .setCustomFieldConfig('barAlignment', 0)
+ .setCustomFieldConfig('spanNulls', false)
+ .setCustomFieldConfig('insertNulls', false)
+ .setCustomFieldConfig('showPoints', VisibilityMode.Auto)
+ .setCustomFieldConfig('pointSize', 5)
+ .setCustomFieldConfig('stacking', { mode: StackingMode.None, group: 'A' })
+ .setCustomFieldConfig('gradientMode', GraphGradientMode.Hue)
+ .setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear })
+ .setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden })
+ .setOption('tooltip', { mode: TooltipDisplayMode.Single })
+
+ .setNoValue('No events found')
+ .build(),
+ });
+}
diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx
index 062ff5b1820..9c9ea58eabe 100644
--- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx
+++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx
@@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
-import { Alert, Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
+import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { combineMatcherStrings } from '../../../utils/alertmanager';
diff --git a/public/app/features/alerting/unified/components/rules/state-history/common.ts b/public/app/features/alerting/unified/components/rules/state-history/common.ts
index f797fa1f8a5..d6d924dccdf 100644
--- a/public/app/features/alerting/unified/components/rules/state-history/common.ts
+++ b/public/app/features/alerting/unified/components/rules/state-history/common.ts
@@ -7,6 +7,7 @@ export interface Line {
current: GrafanaAlertStateWithReason;
values?: Record;
labels?: Record;
+ ruleUID?: string;
}
export interface LogRecord {
diff --git a/public/app/features/alerting/unified/utils/rule-id.ts b/public/app/features/alerting/unified/utils/rule-id.ts
index 3e6ebad8c64..b3785ffcaf5 100644
--- a/public/app/features/alerting/unified/utils/rule-id.ts
+++ b/public/app/features/alerting/unified/utils/rule-id.ts
@@ -240,7 +240,7 @@ export function hashRule(rule: Rule): string {
throw new Error('only recording and alerting rules can be hashed');
}
-function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
+export function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0])));
}
diff --git a/public/app/features/connections/__mocks__/store.navIndex.mock.ts b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
index 217f9891655..f135bf3cf83 100644
--- a/public/app/features/connections/__mocks__/store.navIndex.mock.ts
+++ b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
@@ -149,6 +149,13 @@ export const navIndex: NavIndex = {
icon: 'layer-group',
url: '/alerting/groups',
},
+ {
+ id: 'history',
+ text: 'History',
+ subTitle: 'Alert state history',
+ icon: 'history',
+ url: '/alerting/history',
+ },
{
id: 'alerting-admin',
text: 'Settings',
diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts
index b226871d30b..c47780b31c3 100644
--- a/public/app/types/unified-alerting-dto.ts
+++ b/public/app/types/unified-alerting-dto.ts
@@ -42,6 +42,11 @@ export function isAlertStateWithReason(
return state !== null && state !== undefined && !propAlertingRuleStateValues.includes(state);
}
+export function mapStateWithReasonToReason(state: GrafanaAlertStateWithReason): string {
+ const match = state.match(/\((.*?)\)/);
+ return match ? match[1] : '';
+}
+
export function mapStateWithReasonToBaseState(
state: GrafanaAlertStateWithReason | PromAlertingRuleState
): GrafanaAlertState | PromAlertingRuleState {
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index c83965e3c5a..36fb784122a 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -25,6 +25,14 @@
"user": "User"
}
},
+ "alert-labels": {
+ "button": {
+ "hide": "Hide common labels",
+ "show": {
+ "tooltip": "Show common labels"
+ }
+ }
+ },
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -84,15 +92,15 @@
},
"counts": {
"alertRule_one": "{{count}} alert rule",
- "alertRule_other": "{{count}} alert rules",
+ "alertRule_other": "{{count}} alert rule",
"dashboard_one": "{{count}} dashboard",
- "dashboard_other": "{{count}} dashboards",
+ "dashboard_other": "{{count}} dashboard",
"folder_one": "{{count}} folder",
- "folder_other": "{{count}} folders",
+ "folder_other": "{{count}} folder",
"libraryPanel_one": "{{count}} library panel",
- "libraryPanel_other": "{{count}} library panels",
+ "libraryPanel_other": "{{count}} library panel",
"total_one": "{{count}} item",
- "total_other": "{{count}} items"
+ "total_other": "{{count}} item"
},
"dashboards-tree": {
"collapse-folder-button": "Collapse folder {{title}}",
@@ -138,6 +146,16 @@
"text": "No results found for your query"
}
},
+ "central-alert-history": {
+ "error": "Something went wrong loading the alert state history",
+ "filter": {
+ "button": {
+ "clear": "Clear"
+ },
+ "label": "Filter events",
+ "placeholder": "Filter events in the list with labels"
+ }
+ },
"clipboard-button": {
"inline-toast": {
"success": "Copied"
@@ -758,7 +776,7 @@
},
"modal": {
"body_one": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
- "body_other": "This panel is being used in {{count}} dashboards. Please choose which dashboard to view the panel in:",
+ "body_other": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
"button-cancel": "Cancel",
"button-view-panel1": "View panel in {{label}}...",
"button-view-panel2": "View panel in dashboard...",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index f99ab49211d..bfe2a139e08 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -25,6 +25,14 @@
"user": "Ůşęř"
}
},
+ "alert-labels": {
+ "button": {
+ "hide": "Ħįđę čőmmőʼn ľäþęľş",
+ "show": {
+ "tooltip": "Ŝĥőŵ čőmmőʼn ľäþęľş"
+ }
+ }
+ },
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -84,15 +92,15 @@
},
"counts": {
"alertRule_one": "{{count}} äľęřŧ řūľę",
- "alertRule_other": "{{count}} äľęřŧ řūľęş",
+ "alertRule_other": "{{count}} äľęřŧ řūľę",
"dashboard_one": "{{count}} đäşĥþőäřđ",
- "dashboard_other": "{{count}} đäşĥþőäřđş",
+ "dashboard_other": "{{count}} đäşĥþőäřđ",
"folder_one": "{{count}} ƒőľđęř",
- "folder_other": "{{count}} ƒőľđęřş",
+ "folder_other": "{{count}} ƒőľđęř",
"libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ",
- "libraryPanel_other": "{{count}} ľįþřäřy päʼnęľş",
+ "libraryPanel_other": "{{count}} ľįþřäřy päʼnęľ",
"total_one": "{{count}} įŧęm",
- "total_other": "{{count}} įŧęmş"
+ "total_other": "{{count}} įŧęm"
},
"dashboards-tree": {
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
@@ -138,6 +146,16 @@
"text": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy"
}
},
+ "central-alert-history": {
+ "error": "Ŝőmęŧĥįʼnģ ŵęʼnŧ ŵřőʼnģ ľőäđįʼnģ ŧĥę äľęřŧ şŧäŧę ĥįşŧőřy",
+ "filter": {
+ "button": {
+ "clear": "Cľęäř"
+ },
+ "label": "Fįľŧęř ęvęʼnŧş",
+ "placeholder": "Fįľŧęř ęvęʼnŧş įʼn ŧĥę ľįşŧ ŵįŧĥ ľäþęľş"
+ }
+ },
"clipboard-button": {
"inline-toast": {
"success": "Cőpįęđ"
@@ -758,7 +776,7 @@
},
"modal": {
"body_one": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
- "body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđş. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
+ "body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
"button-cancel": "Cäʼnčęľ",
"button-view-panel1": "Vįęŵ päʼnęľ įʼn {{label}}...",
"button-view-panel2": "Vįęŵ päʼnęľ įʼn đäşĥþőäřđ...",