Internationalisation: Auto mark-up Alerting's strings (#103354)

* Internationalisation: Auto mark-up Alerting code strings

* Update .betterer.results

* Revert prometheus duration docs change

* Fix duplicated import

* Remove unneeded translation
This commit is contained in:
Tom Ratcliffe
2025-04-04 15:56:10 +01:00
committed by GitHub
parent 892ea3ad30
commit a93e618102
171 changed files with 2889 additions and 1447 deletions
+121 -851
View File
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ import { Fragment, useEffect } from 'react';
import { Alert, Box, LoadingPlaceholder, Text } from '@grafana/ui'; import { Alert, Box, LoadingPlaceholder, Text } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Trans, t } from 'app/core/internationalization';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types'; import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types';
@@ -55,7 +56,9 @@ const AlertGroups = () => {
return ( return (
<> <>
<AlertGroupFilter groups={results} /> <AlertGroupFilter groups={results} />
{loading && <LoadingPlaceholder text="Loading notifications" />} {loading && (
<LoadingPlaceholder text={t('alerting.alert-groups.text-loading-notifications', 'Loading notifications')} />
)}
{error && !loading && ( {error && !loading && (
<Alert title={'Error loading notifications'} severity={'error'}> <Alert title={'Error loading notifications'} severity={'error'}>
{error.message || 'Unknown error'} {error.message || 'Unknown error'}
@@ -63,7 +66,12 @@ const AlertGroups = () => {
)} )}
{grafanaAmDeliveryDisabled && ( {grafanaAmDeliveryDisabled && (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager"> <Alert
title={t(
'alerting.alert-groups.title-grafana-alerts-delivered-alertmanager',
'Grafana alerts are not delivered to Grafana Alertmanager'
)}
>
Grafana is configured to send alerts to external alertmanagers only. No alerts are expected to be available Grafana is configured to send alerts to external alertmanagers only. No alerts are expected to be available
here for the selected Alertmanager. here for the selected Alertmanager.
</Alert> </Alert>
@@ -85,7 +93,11 @@ const AlertGroups = () => {
</Fragment> </Fragment>
); );
})} })}
{results && !filteredAlertGroups.length && <p>No results.</p>} {results && !filteredAlertGroups.length && (
<p>
<Trans i18nKey="alerting.alert-groups.no-results">No results.</Trans>
</p>
)}
</> </>
); );
}; };
@@ -3,6 +3,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
interface AlertWarningProps { interface AlertWarningProps {
title: string; title: string;
@@ -12,7 +13,9 @@ export function AlertWarning({ title, children }: AlertWarningProps) {
return ( return (
<Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}> <Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}>
<p>{children}</p> <p>{children}</p>
<LinkButton href="alerting/list">To rule list</LinkButton> <LinkButton href="alerting/list">
<Trans i18nKey="alerting.alert-warning.to-rule-list">To rule list</Trans>
</LinkButton>
</Alert> </Alert>
); );
} }
@@ -6,6 +6,7 @@ import { useDebounce } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui'; import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui';
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants'; import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
import { Trans, t } from 'app/core/internationalization';
import { getQueryParamValue } from 'app/core/utils/query'; import { getQueryParamValue } from 'app/core/utils/query';
import { FolderState, useDispatch } from 'app/types'; import { FolderState, useDispatch } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
@@ -66,7 +67,10 @@ export const AlertsFolderView = ({ folder }: Props) => {
<FilterInput <FilterInput
value={nameFilter} value={nameFilter}
onChange={setNameFilter} onChange={setNameFilter}
placeholder="Search alert rules by name" placeholder={t(
'alerting.alerts-folder-view.name-filter-placeholder-search-alert-rules-by-name',
'Search alert rules by name'
)}
data-testid="name-filter" data-testid="name-filter"
/> />
<Stack direction="row"> <Stack direction="row">
@@ -75,14 +79,17 @@ export const AlertsFolderView = ({ folder }: Props) => {
onChange={({ value }) => value && setSortOrder(value)} onChange={({ value }) => value && setSortOrder(value)}
options={sortOptions} options={sortOptions}
width={25} width={25}
aria-label="Sort" aria-label={t('alerting.alerts-folder-view.aria-label-sort', 'Sort')}
placeholder={`Sort (Default A-Z)`} placeholder={`Sort (Default A-Z)`}
prefix={<Icon name={sortOrder === SortOrder.Ascending ? 'sort-amount-up' : 'sort-amount-down'} />} prefix={<Icon name={sortOrder === SortOrder.Ascending ? 'sort-amount-up' : 'sort-amount-down'} />}
/> />
<FilterInput <FilterInput
value={labelFilter} value={labelFilter}
onChange={setLabelFilter} onChange={setLabelFilter}
placeholder="Search alerts by labels" placeholder={t(
'alerting.alerts-folder-view.label-filter-placeholder-search-alerts-by-labels',
'Search alerts by labels'
)}
className={styles.filterLabelsInput} className={styles.filterLabelsInput}
data-testid="label-filter" data-testid="label-filter"
/> />
@@ -111,7 +118,11 @@ export const AlertsFolderView = ({ folder }: Props) => {
</Card> </Card>
))} ))}
</Stack> </Stack>
{hasNoResults && <div className={styles.noResults}>No alert rules found</div>} {hasNoResults && (
<div className={styles.noResults}>
<Trans i18nKey="alerting.alerts-folder-view.no-alert-rules-found">No alert rules found</Trans>
</div>
)}
<div className={styles.pagination}> <div className={styles.pagination}>
<Pagination <Pagination
currentPage={page} currentPage={page}
@@ -7,6 +7,7 @@ import { config } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema'; import { DataSourceRef } from '@grafana/schema';
import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview'; import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview';
import { Alert, Badge, ErrorBoundaryAlert, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { Alert, Badge, ErrorBoundaryAlert, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { AlertDataQuery, AlertQuery } from '../../../types/unified-alerting-dto'; import { AlertDataQuery, AlertQuery } from '../../../types/unified-alerting-dto';
@@ -244,7 +245,12 @@ function ExpressionPreview({ refId, model, evalData, isAlertCondition }: Express
> >
<div className={styles.previewWrapper}> <div className={styles.previewWrapper}>
{evalData?.errors?.map((error) => ( {evalData?.errors?.map((error) => (
<Alert key={uniqueId()} title="Expression failed" severity="error" bottomSpacing={1}> <Alert
key={uniqueId()}
title={t('alerting.expression-preview.title-expression-failed', 'Expression failed')}
severity="error"
bottomSpacing={1}
>
{error.message} {error.message}
</Alert> </Alert>
))} ))}
@@ -272,12 +278,14 @@ function QueryBox({ refId, headerItems = [], children, isAlertCondition, explore
<span className={styles.refId}>{refId}</span> <span className={styles.refId}>{refId}</span>
{headerItems} {headerItems}
<Spacer /> <Spacer />
{isAlertCondition && <Badge color="green" icon="check" text="Alert condition" />} {isAlertCondition && (
<Badge color="green" icon="check" text={t('alerting.query-box.text-alert-condition', 'Alert condition')} />
)}
{exploreLink && ( {exploreLink && (
<WithReturnButton <WithReturnButton
component={ component={
<LinkButton size="md" variant="secondary" icon="compass" href={exploreLink}> <LinkButton size="md" variant="secondary" icon="compass" href={exploreLink}>
View in Explore <Trans i18nKey="alerting.query-box.view-in-explore">View in Explore</Trans>
</LinkButton> </LinkButton>
} }
/> />
@@ -339,7 +347,9 @@ function ClassicConditionViewer({ model }: { model: ExpressionQuery }) {
{index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.type]?.text} {index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.type]?.text}
</div> </div>
<div className={styles.bold}>{reducer?.type && reducerFunctions[reducer.type]?.text}</div> <div className={styles.bold}>{reducer?.type && reducerFunctions[reducer.type]?.text}</div>
<div className={styles.blue}>OF</div> <div className={styles.blue}>
<Trans i18nKey="alerting.classic-condition-viewer.of">OF</Trans>
</div>
<div className={styles.bold}>{query.params[0]}</div> <div className={styles.bold}>{query.params[0]}</div>
<div className={styles.blue}>{evalFunctions[evaluator.type].text}</div> <div className={styles.blue}>{evalFunctions[evaluator.type].text}</div>
<div className={styles.bold}> <div className={styles.bold}>
@@ -372,13 +382,19 @@ function ReduceConditionViewer({ model }: { model: ExpressionQuery }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.label}>Function</div> <div className={styles.label}>
<Trans i18nKey="alerting.reduce-condition-viewer.function">Function</Trans>
</div>
<div className={styles.value}>{reducerType?.label}</div> <div className={styles.value}>{reducerType?.label}</div>
<div className={styles.label}>Input</div> <div className={styles.label}>
<Trans i18nKey="alerting.reduce-condition-viewer.input">Input</Trans>
</div>
<div className={styles.value}>{expression}</div> <div className={styles.value}>{expression}</div>
<div className={styles.label}>Mode</div> <div className={styles.label}>
<Trans i18nKey="alerting.reduce-condition-viewer.mode">Mode</Trans>
</div>
<div className={styles.value}>{modeName?.label}</div> <div className={styles.value}>{modeName?.label}</div>
</div> </div>
); );
@@ -407,16 +423,24 @@ function ResampleExpressionViewer({ model }: { model: ExpressionQuery }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.label}>Input</div> <div className={styles.label}>
<Trans i18nKey="alerting.resample-expression-viewer.input">Input</Trans>
</div>
<div className={styles.value}>{expression}</div> <div className={styles.value}>{expression}</div>
<div className={styles.label}>Resample to</div> <div className={styles.label}>
<Trans i18nKey="alerting.resample-expression-viewer.resample-to">Resample to</Trans>
</div>
<div className={styles.value}>{window}</div> <div className={styles.value}>{window}</div>
<div className={styles.label}>Downsample</div> <div className={styles.label}>
<Trans i18nKey="alerting.resample-expression-viewer.downsample">Downsample</Trans>
</div>
<div className={styles.value}>{downsamplerType?.label}</div> <div className={styles.value}>{downsamplerType?.label}</div>
<div className={styles.label}>Upsample</div> <div className={styles.label}>
<Trans i18nKey="alerting.resample-expression-viewer.upsample">Upsample</Trans>
</div>
<div className={styles.value}>{upsamplerType?.label}</div> <div className={styles.value}>{upsamplerType?.label}</div>
</div> </div>
); );
@@ -450,7 +474,9 @@ function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) {
return ( return (
<> <>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.label}>Input</div> <div className={styles.label}>
<Trans i18nKey="alerting.threshold-expression-viewer.input">Input</Trans>
</div>
<div className={styles.value}>{expression}</div> <div className={styles.value}>{expression}</div>
{evaluator && ( {evaluator && (
@@ -465,7 +491,9 @@ function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) {
<div className={styles.container}> <div className={styles.container}>
{unloadEvaluator && ( {unloadEvaluator && (
<> <>
<div className={styles.label}>Stop alerting when </div> <div className={styles.label}>
<Trans i18nKey="alerting.threshold-expression-viewer.stop-alerting-when">Stop alerting when </Trans>
</div>
<div className={styles.value}>{expression}</div> <div className={styles.value}>{expression}</div>
<> <>
@@ -507,7 +535,9 @@ function MathExpressionViewer({ model }: { model: ExpressionQuery }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.label}>Input</div> <div className={styles.label}>
<Trans i18nKey="alerting.math-expression-viewer.input">Input</Trans>
</div>
<div className={styles.value}>{expression}</div> <div className={styles.value}>{expression}</div>
</div> </div>
); );
@@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Alert, LoadingPlaceholder, ScrollContainer, useStyles2 } from '@grafana/ui'; import { Alert, LoadingPlaceholder, ScrollContainer, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@@ -31,7 +31,10 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
const canCreateRules = config.unifiedAlertingEnabled && contextSrv.hasPermission(permissions.create); const canCreateRules = config.unifiedAlertingEnabled && contextSrv.hasPermission(permissions.create);
const alert = errors.length ? ( const alert = errors.length ? (
<Alert title="Errors loading rules" severity="error"> <Alert
title={t('alerting.panel-alert-tab-content.alert.title-errors-loading-rules', 'Errors loading rules')}
severity="error"
>
{errors.map((error, index) => ( {errors.map((error, index) => (
<div key={index}>Failed to load Grafana rules state: {stringifyErrorLike(error)}</div> <div key={index}>Failed to load Grafana rules state: {stringifyErrorLike(error)}</div>
))} ))}
@@ -42,7 +45,7 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
return ( return (
<div className={styles.innerWrapper}> <div className={styles.innerWrapper}>
{alert} {alert}
<LoadingPlaceholder text="Loading rules..." /> <LoadingPlaceholder text={t('alerting.panel-alert-tab-content.text-loading-rules', 'Loading rules...')} />
</div> </div>
); );
} }
@@ -77,7 +80,10 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
</> </>
)} )}
{isNew && !!dashboard.meta.canSave && ( {isNew && !!dashboard.meta.canSave && (
<Alert severity="info" title="Dashboard not saved"> <Alert
severity="info"
title={t('alerting.panel-alert-tab-content.title-dashboard-not-saved', 'Dashboard not saved')}
>
<Trans i18nKey="dashboard.panel-edit.alerting-tab.dashboard-not-saved"> <Trans i18nKey="dashboard.panel-edit.alerting-tab.dashboard-not-saved">
Dashboard must be saved before alerts can be added. Dashboard must be saved before alerts can be added.
</Trans> </Trans>
@@ -6,6 +6,7 @@ import { useLocation } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config, isFetchError } from '@grafana/runtime'; import { config, isFetchError } from '@grafana/runtime';
import { Alert, Card, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Alert, Card, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertLabels } from './components/AlertLabels'; import { AlertLabels } from './components/AlertLabels';
import { RuleViewerLayout } from './components/rule-viewer/RuleViewerLayout'; import { RuleViewerLayout } from './components/rule-viewer/RuleViewerLayout';
@@ -76,7 +77,7 @@ export function RedirectToRuleViewer(): JSX.Element | null {
if (loading) { if (loading) {
return ( return (
<RuleViewerLayout title={pageTitle}> <RuleViewerLayout title={pageTitle}>
<LoadingPlaceholder text="Loading rule..." /> <LoadingPlaceholder text={t('alerting.redirect-to-rule-viewer.text-loading-rule', 'Loading rule...')} />
</RuleViewerLayout> </RuleViewerLayout>
); );
} }
@@ -86,7 +87,7 @@ export function RedirectToRuleViewer(): JSX.Element | null {
if (!rulesSource) { if (!rulesSource) {
return ( return (
<RuleViewerLayout title={pageTitle}> <RuleViewerLayout title={pageTitle}>
<Alert title="Could not view rule"> <Alert title={t('alerting.redirect-to-rule-viewer.title-could-not-view-rule', 'Could not view rule')}>
<details className={styles.errorMessage}>{`Could not find data source with name: ${sourceName}.`}</details> <details className={styles.errorMessage}>{`Could not find data source with name: ${sourceName}.`}</details>
</Alert> </Alert>
</RuleViewerLayout> </RuleViewerLayout>
@@ -1,4 +1,5 @@
import { LinkButton, Stack, Text } from '@grafana/ui'; import { LinkButton, Stack, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { WithReturnButton } from './components/WithReturnButton'; import { WithReturnButton } from './components/WithReturnButton';
@@ -27,10 +28,10 @@ function SettingsContent() {
actions={[ actions={[
<WithReturnButton <WithReturnButton
key="add-alertmanager" key="add-alertmanager"
title="Alerting settings" title={t('alerting.settings-content.title-alerting-settings', 'Alerting settings')}
component={ component={
<LinkButton href="/connections/datasources/alertmanager" icon="plus" variant="primary"> <LinkButton href="/connections/datasources/alertmanager" icon="plus" variant="primary">
Add new Alertmanager <Trans i18nKey="alerting.settings-content.add-new-alertmanager">Add new Alertmanager</Trans>
</LinkButton> </LinkButton>
} }
/>, />,
@@ -38,10 +39,14 @@ function SettingsContent() {
> >
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
{/* Grafana built-in Alertmanager */} {/* Grafana built-in Alertmanager */}
<Text variant="h5">Built-in Alertmanager</Text> <Text variant="h5">
<Trans i18nKey="alerting.settings-content.builtin-alertmanager">Built-in Alertmanager</Trans>
</Text>
<InternalAlertmanager onEditConfiguration={showConfiguration} /> <InternalAlertmanager onEditConfiguration={showConfiguration} />
{/* other (external Alertmanager data sources we have added to Grafana such as vanilla, Mimir, Cortex) */} {/* other (external Alertmanager data sources we have added to Grafana such as vanilla, Mimir, Cortex) */}
<Text variant="h5">Other Alertmanagers</Text> <Text variant="h5">
<Trans i18nKey="alerting.settings-content.other-alertmanagers">Other Alertmanagers</Trans>
</Text>
<ExternalAlertmanagers onEditConfiguration={showConfiguration} /> <ExternalAlertmanagers onEditConfiguration={showConfiguration} />
</Stack> </Stack>
{configurationDrawer} {configurationDrawer}
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, useStyles2 } from '@grafana/ui'; import { IconButton, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
interface Props { interface Props {
labelKey: string; labelKey: string;
@@ -18,7 +19,14 @@ export const AlertLabel = ({ labelKey, value, operator = '=', onRemoveLabel }: P
{labelKey} {labelKey}
{operator} {operator}
{value} {value}
{!!onRemoveLabel && <IconButton name="times" size="xs" onClick={onRemoveLabel} tooltip="Remove label" />} {!!onRemoveLabel && (
<IconButton
name="times"
size="xs"
onClick={onRemoveLabel}
tooltip={t('alerting.alert-label.tooltip-remove-label', 'Remove label')}
/>
)}
</div> </div>
); );
}; };
@@ -33,7 +33,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size, onClick }: Props)
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels'); const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
return ( return (
<div className={styles.wrapper} role="list" aria-label="Labels"> <div className={styles.wrapper} role="list" aria-label={t('alerting.alert-labels.aria-label-labels', 'Labels')}>
{labelsToShow.map(([label, value]) => { {labelsToShow.map(([label, value]) => {
return ( return (
<Label <Label
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, useStyles2 } from '@grafana/ui'; import { Alert, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../api/alertmanagerApi'; import { alertmanagerApi } from '../api/alertmanagerApi';
@@ -40,7 +41,12 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.External) { if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.External) {
return ( return (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager"> <Alert
title={t(
'alerting.grafana-alertmanager-delivery-warning.title-grafana-alerts-delivered-alertmanager',
'Grafana alerts are not delivered to Grafana Alertmanager'
)}
>
Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration
will not affect delivery of your alerts. will not affect delivery of your alerts.
<div className={styles.adminHint}> <div className={styles.adminHint}>
@@ -53,7 +59,13 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.All && hasActiveExternalAMs) { if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.All && hasActiveExternalAMs) {
return ( return (
<Alert title="You have additional Alertmanagers to configure" severity="warning"> <Alert
title={t(
'alerting.grafana-alertmanager-delivery-warning.title-you-have-additional-alertmanagers-to-configure',
'You have additional Alertmanagers to configure'
)}
severity="warning"
>
Ensure you make configuration changes in the correct Alertmanagers; both internal and external. Changing one Ensure you make configuration changes in the correct Alertmanagers; both internal and external. Changing one
will not affect the others. will not affect the others.
<div className={styles.adminHint}> <div className={styles.adminHint}>
@@ -1,8 +1,15 @@
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Alert } from '@grafana/ui'; import { Alert } from '@grafana/ui';
import { t } from 'app/core/internationalization';
const EvaluationIntervalLimitExceeded = () => ( const EvaluationIntervalLimitExceeded = () => (
<Alert severity="warning" title="Global evaluation interval limit exceeded"> <Alert
severity="warning"
title={t(
'alerting.evaluation-interval-limit-exceeded.title-global-evaluation-interval-limit-exceeded',
'Global evaluation interval limit exceeded'
)}
>
A minimum evaluation interval of <strong>{config.unifiedAlerting.minInterval}</strong> has been configured in A minimum evaluation interval of <strong>{config.unifiedAlerting.minInterval}</strong> has been configured in
Grafana. Grafana.
<br /> <br />
@@ -1,4 +1,5 @@
import { Alert } from '@grafana/ui'; import { Alert } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertManagerDataSource } from '../utils/datasource'; import { AlertManagerDataSource } from '../utils/datasource';
@@ -7,13 +8,22 @@ interface Props {
} }
const NoAlertManagersAvailable = () => ( const NoAlertManagersAvailable = () => (
<Alert title="No Alertmanager found" severity="warning"> <Alert
title={t('alerting.no-alert-managers-available.title-no-alertmanager-found', 'No Alertmanager found')}
severity="warning"
>
We could not find any external Alertmanagers and you may not have access to the built-in Grafana Alertmanager. We could not find any external Alertmanagers and you may not have access to the built-in Grafana Alertmanager.
</Alert> </Alert>
); );
const OtherAlertManagersAvailable = () => ( const OtherAlertManagersAvailable = () => (
<Alert title="Selected Alertmanager not found." severity="warning"> <Alert
title={t(
'alerting.other-alert-managers-available.title-selected-alertmanager-not-found',
'Selected Alertmanager not found.'
)}
severity="warning"
>
The selected Alertmanager no longer exists or you may not have permission to access it. You can select a different The selected Alertmanager no longer exists or you may not have permission to access it. You can select a different
Alertmanager from the dropdown. Alertmanager from the dropdown.
</Alert> </Alert>
@@ -1,7 +1,7 @@
import { ComponentPropsWithoutRef } from 'react'; import { ComponentPropsWithoutRef } from 'react';
import { Alert, Badge, Tooltip } from '@grafana/ui'; import { Alert, Badge, Tooltip } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
export enum ProvisionedResource { export enum ProvisionedResource {
ContactPoint = 'contact point', ContactPoint = 'contact point',
@@ -37,7 +37,7 @@ export const ProvisioningBadge = ({
*/ */
provenance?: string; provenance?: string;
}) => { }) => {
const badge = <Badge text="Provisioned" color="purple" />; const badge = <Badge text={t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned')} color="purple" />;
if (tooltip) { if (tooltip) {
const provenanceTooltip = ( const provenanceTooltip = (
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertState, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; import { AlertState, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
@@ -41,7 +42,7 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
icon={'bell'} icon={'bell'}
size={'sm'} size={'sm'}
> >
Manage silences <Trans i18nKey="alerting.alert-details.manage-silences">Manage silences</Trans>
</LinkButton> </LinkButton>
</Authorize> </Authorize>
)} )}
@@ -53,7 +54,7 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
icon={'bell-slash'} icon={'bell-slash'}
size={'sm'} size={'sm'}
> >
Silence <Trans i18nKey="alerting.alert-details.silence">Silence</Trans>
</LinkButton> </LinkButton>
</Authorize> </Authorize>
)} )}
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack, TextLink, useStyles2 } from '@grafana/ui'; import { Stack, TextLink, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
import { createContactPointSearchLink } from '../../utils/misc'; import { createContactPointSearchLink } from '../../utils/misc';
@@ -55,7 +56,9 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
)} )}
</Stack> </Stack>
) : ( ) : (
<span>No grouping</span> <span>
<Trans i18nKey="alerting.alert-group.no-grouping">No grouping</Trans>
</span>
)} )}
</div> </div>
<AlertGroupHeader group={group} /> <AlertGroupHeader group={group} />
@@ -4,6 +4,7 @@ import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui'; import { Button, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Trans } from 'app/core/internationalization';
import { AlertState, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; import { AlertState, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
import { getFiltersFromUrlParams } from '../../utils/misc'; import { getFiltersFromUrlParams } from '../../utils/misc';
@@ -55,7 +56,7 @@ export const AlertGroupFilter = ({ groups }: Props) => {
/> />
{showClearButton && ( {showClearButton && (
<Button className={styles.clearButton} variant={'secondary'} icon="times" onClick={clearFilters}> <Button className={styles.clearButton} variant={'secondary'} icon="times" onClick={clearFilters}>
Clear filters <Trans i18nKey="alerting.alert-group-filter.clear-filters">Clear filters</Trans>
</Button> </Button>
)} )}
</div> </div>
@@ -1,5 +1,6 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Icon, Label, RadioButtonGroup, Tooltip } from '@grafana/ui'; import { Icon, Label, RadioButtonGroup, Tooltip } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { AlertState } from 'app/plugins/datasource/alertmanager/types'; import { AlertState } from 'app/plugins/datasource/alertmanager/types';
interface Props { interface Props {
@@ -27,7 +28,11 @@ export const AlertStateFilter = ({ onStateFilterChange, stateFilter }: Props) =>
Active: The alert notification has been handled. The alert is still firing and continues to be Active: The alert notification has been handled. The alert is still firing and continues to be
managed. managed.
</li> </li>
<li>Suppressed: The alert has been silenced.</li> <li>
<Trans i18nKey="alerting.alert-state-filter.suppressed-the-alert-has-been-silenced">
Suppressed: The alert has been silenced.
</Trans>
</li>
<li>Unprocessed: The alert is received but its notification has not been processed yet.</li> <li>Unprocessed: The alert is received but its notification has not been processed yet.</li>
</ul> </ul>
</div> </div>
@@ -2,6 +2,7 @@ import { uniq } from 'lodash';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Icon, Label, MultiSelect, Tooltip } from '@grafana/ui'; import { Icon, Label, MultiSelect, Tooltip } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
import { isPrivateLabelKey } from '../../utils/labels'; import { isPrivateLabelKey } from '../../utils/labels';
@@ -38,7 +39,7 @@ export const GroupBy = ({ groups, groupBy, onGroupingChange }: Props) => {
<MultiSelect <MultiSelect
aria-label={'group by label keys'} aria-label={'group by label keys'}
value={groupBy} value={groupBy}
placeholder="Group by" placeholder={t('alerting.group-by.placeholder-group-by', 'Group by')}
prefix={<Icon name={'tag-alt'} />} prefix={<Icon name={'tag-alt'} />}
onChange={(items) => { onChange={(items) => {
onGroupingChange(items.map(({ value }) => value as string)); onGroupingChange(items.map(({ value }) => value as string));
@@ -4,6 +4,7 @@ import { useDebounce } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { LogMessages, logInfo } from '../../Analytics'; import { LogMessages, logInfo } from '../../Analytics';
import { parsePromQLStyleMatcherLoose } from '../../utils/matchers'; import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
@@ -47,7 +48,9 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
label={ label={
<Label> <Label>
<Stack gap={0.5} alignItems="center"> <Stack gap={0.5} alignItems="center">
<span>Search by label</span> <span>
<Trans i18nKey="alerting.matcher-filter.search-by-label">Search by label</Trans>
</span>
<Tooltip <Tooltip
content={ content={
<div> <div>
@@ -70,7 +73,7 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
} }
> >
<Input <Input
placeholder="Search" placeholder={t('alerting.matcher-filter.search-query-input-placeholder-search', 'Search')}
value={filterQuery} value={filterQuery}
onChange={(e) => setFilterQuery(e.currentTarget.value)} onChange={(e) => setFilterQuery(e.currentTarget.value)}
data-testid="search-query-input" data-testid="search-query-input"
@@ -1,5 +1,6 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { MultiSelect, MultiSelectCommonProps } from '@grafana/ui'; import { MultiSelect, MultiSelectCommonProps } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useSelectableMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; import { useSelectableMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'; import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager'; import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager';
@@ -22,9 +23,9 @@ const MuteTimingsSelector = ({
return ( return (
<MultiSelect <MultiSelect
aria-label="Mute timings" aria-label={t('alerting.mute-timings-selector.aria-label-mute-timings', 'Mute timings')}
options={muteTimingOptions} options={muteTimingOptions}
placeholder="Select mute timings..." placeholder={t('alerting.mute-timings-selector.placeholder-select-mute-timings', 'Select mute timings...')}
{...selectProps} {...selectProps}
/> />
); );
@@ -1,4 +1,5 @@
import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui'; import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { usePluginBridge } from '../../hooks/usePluginBridge'; import { usePluginBridge } from '../../hooks/usePluginBridge';
import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config'; import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config';
@@ -25,19 +26,19 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P
<> <>
{loading === true && ( {loading === true && (
<Button icon="fire" size="sm" type="button" disabled> <Button icon="fire" size="sm" type="button" disabled>
Declare Incident <Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
</Button> </Button>
)} )}
{installed === false && ( {installed === false && (
<Tooltip content={'Grafana Incident is not installed or is not configured correctly'}> <Tooltip content={'Grafana Incident is not installed or is not configured correctly'}>
<Button icon="fire" size="sm" type="button" disabled> <Button icon="fire" size="sm" type="button" disabled>
Declare Incident <Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
{settings && ( {settings && (
<LinkButton icon="fire" size="sm" type="button" href={bridgeURL}> <LinkButton icon="fire" size="sm" type="button" href={bridgeURL}>
Declare Incident <Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
</LinkButton> </LinkButton>
)} )}
</> </>
@@ -55,13 +56,29 @@ export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }:
return ( return (
<> <>
{loading === true && <Menu.Item label="Declare incident" icon="fire" disabled />} {loading === true && (
<Menu.Item
label={t('alerting.declare-incident-menu-item.label-declare-incident', 'Declare incident')}
icon="fire"
disabled
/>
)}
{installed === false && ( {installed === false && (
<Tooltip content={'Grafana Incident is not installed or is not configured correctly'}> <Tooltip content={'Grafana Incident is not installed or is not configured correctly'}>
<Menu.Item label="Declare incident" icon="fire" disabled /> <Menu.Item
label={t('alerting.declare-incident-menu-item.label-declare-incident', 'Declare incident')}
icon="fire"
disabled
/>
</Tooltip> </Tooltip>
)} )}
{settings && <Menu.Item label="Declare incident" url={bridgeURL} icon="fire" />} {settings && (
<Menu.Item
label={t('alerting.declare-incident-menu-item.label-declare-incident', 'Declare incident')}
url={bridgeURL}
icon="fire"
/>
)}
</> </>
); );
}; };
@@ -86,7 +86,11 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
if (showManagePermissions) { if (showManagePermissions) {
menuActions.push( menuActions.push(
<Fragment key="manage-permissions"> <Fragment key="manage-permissions">
<Menu.Item icon="unlock" label="Manage permissions" onClick={() => setShowPermissionsDrawer(true)} /> <Menu.Item
icon="unlock"
label={t('alerting.contact-point-header.label-manage-permissions', 'Manage permissions')}
onClick={() => setShowPermissionsDrawer(true)}
/>
</Fragment> </Fragment>
); );
} }
@@ -96,7 +100,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
<Fragment key="export-contact-point"> <Fragment key="export-contact-point">
<Menu.Item <Menu.Item
icon="download-alt" icon="download-alt"
label="Export" label={t('alerting.contact-point-header.export-label-export', 'Export')}
ariaLabel="export" ariaLabel="export"
disabled={!exportAllowed} disabled={!exportAllowed}
data-testid="export" data-testid="export"
@@ -155,7 +159,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
)} )}
> >
<Menu.Item <Menu.Item
label="Delete" label={t('alerting.contact-point-header.label-delete', 'Delete')}
ariaLabel="delete" ariaLabel="delete"
icon="trash-alt" icon="trash-alt"
destructive destructive
@@ -72,7 +72,7 @@ const ContactPointsTab = () => {
const search = queryParams.get('search'); const search = queryParams.get('search');
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading..." />; return <LoadingPlaceholder text={t('alerting.contact-points-tab.text-loading', 'Loading...')} />;
} }
const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
@@ -107,7 +107,7 @@ const ContactPointsTab = () => {
{addContactPointSupported && ( {addContactPointSupported && (
<LinkButton <LinkButton
icon="plus" icon="plus"
aria-label="add contact point" aria-label={t('alerting.contact-points-tab.aria-label-add-contact-point', 'add contact point')}
variant="primary" variant="primary"
href="/alerting/notifications/receivers/new" href="/alerting/notifications/receivers/new"
disabled={!addContactPointAllowed} disabled={!addContactPointAllowed}
@@ -119,17 +119,24 @@ const ContactPointsTab = () => {
<Button <Button
icon="download-alt" icon="download-alt"
variant="secondary" variant="secondary"
aria-label="export all" aria-label={t('alerting.contact-points-tab.aria-label-export-all', 'export all')}
disabled={!exportContactPointsAllowed} disabled={!exportContactPointsAllowed}
onClick={() => showExportDrawer(ALL_CONTACT_POINTS)} onClick={() => showExportDrawer(ALL_CONTACT_POINTS)}
> >
Export all <Trans i18nKey="alerting.contact-points-tab.export-all">Export all</Trans>
</Button> </Button>
)} )}
</Stack> </Stack>
</Stack> </Stack>
{error ? ( {error ? (
<Alert title="Failed to fetch contact points">{stringifyErrorLike(error)}</Alert> <Alert
title={t(
'alerting.contact-points-tab.title-failed-to-fetch-contact-points',
'Failed to fetch contact points'
)}
>
{stringifyErrorLike(error)}
</Alert>
) : ( ) : (
<ContactPointsList contactPoints={contactPoints} search={search} pageSize={DEFAULT_PAGE_SIZE} /> <ContactPointsList contactPoints={contactPoints} search={search} pageSize={DEFAULT_PAGE_SIZE} />
)} )}
@@ -149,7 +156,9 @@ const NotificationTemplatesTab = () => {
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between"> <Stack direction="row" alignItems="center" justifyContent="space-between">
<Text variant="body" color="secondary"> <Text variant="body" color="secondary">
Create notification templates to customize your notifications. <Trans i18nKey="alerting.notification-templates-tab.create-notification-templates-customize-notifications">
Create notification templates to customize your notifications.
</Trans>
</Text> </Text>
{createTemplateSupported && ( {createTemplateSupported && (
<LinkButton <LinkButton
@@ -158,7 +167,9 @@ const NotificationTemplatesTab = () => {
href="/alerting/notifications/templates/new" href="/alerting/notifications/templates/new"
disabled={!createTemplateAllowed} disabled={!createTemplateAllowed}
> >
Add notification template group <Trans i18nKey="alerting.notification-templates-tab.add-notification-template-group">
Add notification template group
</Trans>
</LinkButton> </LinkButton>
)} )}
</Stack> </Stack>
@@ -211,7 +222,7 @@ export const ContactPointsPageContents = () => {
<TabsBar> <TabsBar>
{showContactPointsTab && ( {showContactPointsTab && (
<Tab <Tab
label="Contact Points" label={t('alerting.contact-points-page-contents.label-contact-points', 'Contact Points')}
active={showingContactPoints} active={showingContactPoints}
counter={contactPoints.length} counter={contactPoints.length}
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)} onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
@@ -219,7 +230,7 @@ export const ContactPointsPageContents = () => {
)} )}
{showTemplatesTab && ( {showTemplatesTab && (
<Tab <Tab
label="Notification Templates" label={t('alerting.contact-points-page-contents.label-notification-templates', 'Notification Templates')}
active={showNotificationTemplates} active={showNotificationTemplates}
onChangeTab={() => setActiveTab(ActiveTab.NotificationTemplates)} onChangeTab={() => setActiveTab(ActiveTab.NotificationTemplates)}
/> />
@@ -48,14 +48,27 @@ const DuplicateMessageTemplateComponent = () => {
} }
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading notification template" />; return (
<LoadingPlaceholder
text={t(
'alerting.duplicate-message-template.text-loading-notification-template',
'Loading notification template'
)}
/>
);
} }
if (error) { if (error) {
return isNotFoundError(error) ? ( return isNotFoundError(error) ? (
notFoundComponent notFoundComponent
) : ( ) : (
<Alert title="Error loading notification template" severity="error"> <Alert
title={t(
'alerting.duplicate-message-template.title-error-loading-notification-template',
'Error loading notification template'
)}
severity="error"
>
{stringifyErrorLike(error)} {stringifyErrorLike(error)}
</Alert> </Alert>
); );
@@ -1,6 +1,7 @@
import { useParams } from 'react-router-dom-v5-compat'; import { useParams } from 'react-router-dom-v5-compat';
import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useGetContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useGetContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc'; import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
@@ -21,12 +22,15 @@ const EditContactPoint = () => {
} = useGetContactPoint({ name: contactPointName, alertmanager: selectedAlertmanager! }); } = useGetContactPoint({ name: contactPointName, alertmanager: selectedAlertmanager! });
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading..." />; return <LoadingPlaceholder text={t('alerting.edit-contact-point.text-loading', 'Loading...')} />;
} }
if (error) { if (error) {
return ( return (
<Alert severity="error" title="Failed to fetch contact point"> <Alert
severity="error"
title={t('alerting.edit-contact-point.title-failed-to-fetch-contact-point', 'Failed to fetch contact point')}
>
{stringifyErrorLike(error)} {stringifyErrorLike(error)}
</Alert> </Alert>
); );
@@ -34,7 +38,7 @@ const EditContactPoint = () => {
if (!contactPoint) { if (!contactPoint) {
return ( return (
<Alert severity="error" title="Receiver not found"> <Alert severity="error" title={t('alerting.edit-contact-point.title-receiver-not-found', 'Receiver not found')}>
{'Sorry, this contact point does not seem to exist.'} {'Sorry, this contact point does not seem to exist.'}
</Alert> </Alert>
); );
@@ -32,14 +32,22 @@ const EditMessageTemplateComponent = () => {
} }
if (isLoading || isUninitialized) { if (isLoading || isUninitialized) {
return <LoadingPlaceholder text="Loading template..." />; return (
<LoadingPlaceholder text={t('alerting.edit-message-template.text-loading-template', 'Loading template...')} />
);
} }
if (error) { if (error) {
return isNotFoundError(error) ? ( return isNotFoundError(error) ? (
notFoundComponent notFoundComponent
) : ( ) : (
<Alert severity="error" title="Failed to fetch notification template"> <Alert
severity="error"
title={t(
'alerting.edit-message-template.title-failed-to-fetch-notification-template',
'Failed to fetch notification template'
)}
>
{stringifyErrorLike(error)} {stringifyErrorLike(error)}
</Alert> </Alert>
); );
@@ -1,4 +1,5 @@
import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useAlertmanager } from '../../state/AlertmanagerContext'; import { useAlertmanager } from '../../state/AlertmanagerContext';
import { stringifyErrorLike } from '../../utils/misc'; import { stringifyErrorLike } from '../../utils/misc';
@@ -11,11 +12,27 @@ export const NotificationTemplates = () => {
const { data: templates, isLoading, error } = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' }); const { data: templates, isLoading, error } = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' });
if (error) { if (error) {
return <Alert title="Failed to fetch notification templates">{stringifyErrorLike(error)}</Alert>; return (
<Alert
title={t(
'alerting.notification-templates.title-failed-to-fetch-notification-templates',
'Failed to fetch notification templates'
)}
>
{stringifyErrorLike(error)}
</Alert>
);
} }
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading notification templates" />; return (
<LoadingPlaceholder
text={t(
'alerting.notification-templates.text-loading-notification-templates',
'Loading notification templates'
)}
/>
);
} }
if (templates) { if (templates) {
@@ -3,6 +3,7 @@ import { useCallback, useState } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; import { Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useURLSearchParams } from '../../../hooks/useURLSearchParams'; import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
@@ -32,10 +33,13 @@ const ContactPointsFilter = () => {
return ( return (
<Stack direction="row" alignItems="end" gap={0.5}> <Stack direction="row" alignItems="end" gap={0.5}>
<Field className={styles.noBottom} label="Search by name or type"> <Field
className={styles.noBottom}
label={t('alerting.contact-points-filter.label-search-by-name-or-type', 'Search by name or type')}
>
<Input <Input
aria-label="search contact points" aria-label={t('alerting.contact-points-filter.aria-label-search-contact-points', 'search contact points')}
placeholder="Search" placeholder={t('alerting.contact-points-filter.placeholder-search', 'Search')}
width={46} width={46}
prefix={<Icon name="search" />} prefix={<Icon name="search" />}
onChange={(event) => { onChange={(event) => {
@@ -44,8 +48,14 @@ const ContactPointsFilter = () => {
value={searchValue} value={searchValue}
/> />
</Field> </Field>
<Button variant="secondary" icon="times" onClick={() => clear()} disabled={!hasInput} aria-label="clear"> <Button
Clear variant="secondary"
icon="times"
onClick={() => clear()}
disabled={!hasInput}
aria-label={t('alerting.contact-points-filter.aria-label-clear', 'clear')}
>
<Trans i18nKey="alerting.contact-points-filter.clear">Clear</Trans>
</Button> </Button>
</Stack> </Stack>
); );
@@ -1,4 +1,5 @@
import { Alert } from '@grafana/ui'; import { Alert } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig'; import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../../state/AlertmanagerContext'; import { useAlertmanager } from '../../../state/AlertmanagerContext';
@@ -16,7 +17,13 @@ const GlobalConfig = () => {
if (error) { if (error) {
return ( return (
<Alert severity="error" title="Failed to fetch notification template"> <Alert
severity="error"
title={t(
'alerting.global-config.title-failed-to-fetch-notification-template',
'Failed to fetch notification template'
)}
>
{String(error)} {String(error)}
</Alert> </Alert>
); );
@@ -1,4 +1,5 @@
import { Alert, LinkButton } from '@grafana/ui'; import { Alert, LinkButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertmanagerAction } from '../../../hooks/useAbilities'; import { AlertmanagerAction } from '../../../hooks/useAbilities';
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource'; import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
@@ -14,7 +15,13 @@ export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps)
return ( return (
<Authorize actions={[AlertmanagerAction.UpdateExternalConfiguration]}> <Authorize actions={[AlertmanagerAction.UpdateExternalConfiguration]}>
<Alert severity="info" title="Global config for contact points"> <Alert
severity="info"
title={t(
'alerting.global-config-alert.title-global-config-for-contact-points',
'Global config for contact points'
)}
>
<p> <p>
For each external Alertmanager you can define global settings, like server addresses, usernames and password, For each external Alertmanager you can define global settings, like server addresses, usernames and password,
for all the supported contact points. for all the supported contact points.
@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { Button, Modal, ModalProps } from '@grafana/ui'; import { Button, Modal, ModalProps } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { stringifyErrorLike } from '../../../utils/misc'; import { stringifyErrorLike } from '../../../utils/misc';
@@ -54,10 +54,21 @@ export const useDeleteContactPointModal = (
onDismiss={handleDismiss} onDismiss={handleDismiss}
closeOnBackdropClick={!isLoading} closeOnBackdropClick={!isLoading}
closeOnEscape={!isLoading} closeOnEscape={!isLoading}
title="Delete contact point" title={t(
'alerting.use-delete-contact-point-modal.modal-element.title-delete-contact-point',
'Delete contact point'
)}
> >
<p>Deleting this contact point will permanently remove it.</p> <p>
<p>Are you sure you want to delete this contact point?</p> <Trans i18nKey="alerting.use-delete-contact-point-modal.modal-element.deleting-contact-point-permanently-remove">
Deleting this contact point will permanently remove it.
</Trans>
</p>
<p>
<Trans i18nKey="alerting.use-delete-contact-point-modal.modal-element.delete-contact-point">
Are you sure you want to delete this contact point?
</Trans>
</p>
<Modal.ButtonRow> <Modal.ButtonRow>
<Button type="button" variant="destructive" onClick={handleSubmit} disabled={isLoading}> <Button type="button" variant="destructive" onClick={handleSubmit} disabled={isLoading}>
@@ -85,7 +96,11 @@ const ErrorModal = ({ isOpen, onDismiss, error }: ErrorModalProps) => (
closeOnEscape={true} closeOnEscape={true}
title={'Something went wrong'} title={'Something went wrong'}
> >
<p>Failed to update your configuration:</p> <p>
<Trans i18nKey="alerting.error-modal.failed-to-update-your-configuration">
Failed to update your configuration:
</Trans>
</p>
<pre> <pre>
<code>{stringifyErrorLike(error)}</code> <code>{stringifyErrorLike(error)}</code>
</pre> </pre>
@@ -1,9 +1,10 @@
import { Badge } from '@grafana/ui'; import { Badge } from '@grafana/ui';
import { t } from 'app/core/internationalization';
export const UnusedContactPointBadge = () => ( export const UnusedContactPointBadge = () => (
<Badge <Badge
text="Unused" text={t('alerting.unused-contact-point-badge.text-unused', 'Unused')}
aria-label="unused" aria-label={t('alerting.unused-contact-point-badge.aria-label-unused', 'unused')}
color="orange" color="orange"
icon="exclamation-triangle" icon="exclamation-triangle"
tooltip="This contact point is not used in any notification policy or alert rule" tooltip="This contact point is not used in any notification policy or alert rule"
@@ -63,10 +63,10 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on
<Trans i18nKey="alerting.common.cancel">Cancel</Trans> <Trans i18nKey="alerting.common.cancel">Cancel</Trans>
</Button> </Button>
<ClipboardButton icon="copy" getText={() => textDefinition}> <ClipboardButton icon="copy" getText={() => textDefinition}>
Copy code <Trans i18nKey="alerting.file-export-preview.copy-code">Copy code</Trans>
</ClipboardButton> </ClipboardButton>
<Button icon="download-alt" onClick={onDownload}> <Button icon="download-alt" onClick={onDownload}>
Download <Trans i18nKey="alerting.file-export-preview.download">Download</Trans>
</Button> </Button>
</div> </div>
</div> </div>
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { RuleIdentifier } from '../../../../../types/unified-alerting'; import { RuleIdentifier } from '../../../../../types/unified-alerting';
import { useRuleWithLocation } from '../../hooks/useCombinedRule'; import { useRuleWithLocation } from '../../hooks/useCombinedRule';
@@ -23,7 +24,7 @@ function GrafanaModifyExport() {
if (!ruleIdentifier) { if (!ruleIdentifier) {
return ( return (
<Alert title="Invalid rule ID" severity="error"> <Alert title={t('alerting.grafana-modify-export.title-invalid-rule-id', 'Invalid rule ID')} severity="error">
The rule UID in the page URL is invalid. Please check the URL and try again. The rule UID in the page URL is invalid. Please check the URL and try again.
</Alert> </Alert>
); );
@@ -36,12 +37,15 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
const { loading, error, result: rulerRule } = useRuleWithLocation({ ruleIdentifier: ruleIdentifier }); const { loading, error, result: rulerRule } = useRuleWithLocation({ ruleIdentifier: ruleIdentifier });
if (loading) { if (loading) {
return <LoadingPlaceholder text="Loading the rule..." />; return <LoadingPlaceholder text={t('alerting.rule-modify-export.text-loading-the-rule', 'Loading the rule...')} />;
} }
if (error) { if (error) {
return ( return (
<Alert title="Cannot load modify export" severity="error"> <Alert
title={t('alerting.rule-modify-export.title-cannot-load-modify-export', 'Cannot load modify export')}
severity="error"
>
{stringifyErrorLike(error)} {stringifyErrorLike(error)}
</Alert> </Alert>
); );
@@ -51,7 +55,7 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
// alert rule does not exist // alert rule does not exist
return ( return (
<Alert <Alert
title="Cannot load the rule. The rule does not exist" title={t('alerting.rule-modify-export.title-cannot-exist', 'Cannot load the rule. The rule does not exist')}
buttonContent="Go back to alert list" buttonContent="Go back to alert list"
onRemove={() => locationService.replace(createRelativeUrl('/alerting/list'))} onRemove={() => locationService.replace(createRelativeUrl('/alerting/list'))}
/> />
@@ -62,7 +66,10 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
// alert rule exists but is not a grafana-managed rule // alert rule exists but is not a grafana-managed rule
return ( return (
<Alert <Alert
title="This rule is not a Grafana-managed alert rule" title={t(
'alerting.rule-modify-export.title-grafanamanaged-alert',
'This rule is not a Grafana-managed alert rule'
)}
buttonContent="Go back to alert list" buttonContent="Go back to alert list"
onRemove={() => locationService.replace(createRelativeUrl('/alerting/list'))} onRemove={() => locationService.replace(createRelativeUrl('/alerting/list'))}
/> />
@@ -78,7 +85,7 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
); );
} }
return <Alert title="Unknown error" />; return <Alert title={t('alerting.rule-modify-export.title-unknown-error', 'Unknown error')} />;
} }
function GrafanaModifyExportPage() { function GrafanaModifyExportPage() {
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -19,7 +20,9 @@ const GrafanaMuteTimingsExporterPreview = ({ exportFormat, onClose }: MuteTiming
const downloadFileName = `mute-timings-${new Date().getTime()}`; const downloadFileName = `mute-timings-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return (
<LoadingPlaceholder text={t('alerting.grafana-mute-timings-exporter-preview.text-loading', 'Loading....')} />
);
} }
return ( return (
<FileExportPreview <FileExportPreview
@@ -46,7 +49,7 @@ const GrafanaMuteTimingExporterPreview = ({
const downloadFileName = `mute-timing-${muteTimingName}-${new Date().getTime()}`; const downloadFileName = `mute-timing-${muteTimingName}-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-mute-timing-exporter-preview.text-loading', 'Loading....')} />;
} }
return ( return (
<FileExportPreview <FileExportPreview
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -20,7 +21,7 @@ const GrafanaPoliciesExporterPreview = ({ exportFormat, onClose }: GrafanaPolici
const downloadFileName = `policies-${new Date().getTime()}`; const downloadFileName = `policies-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-policies-exporter-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -30,7 +31,7 @@ const GrafanaReceiverExportPreview = ({
const downloadFileName = `cp-${receiverName}-${new Date().getTime()}`; const downloadFileName = `cp-${receiverName}-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-receiver-export-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -23,7 +24,7 @@ const GrafanaReceiversExportPreview = ({ decrypt, exportFormat, onClose }: Grafa
const downloadFileName = `contact-points-${new Date().getTime()}`; const downloadFileName = `contact-points-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-receivers-export-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -23,7 +24,7 @@ const GrafanaRuleExportPreview = ({ alertUid, exportFormat, onClose }: GrafanaRu
const downloadFileName = `${alertUid}-${new Date().getTime()}`; const downloadFileName = `${alertUid}-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-rule-export-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { FolderDTO } from '../../../../../types'; import { FolderDTO } from '../../../../../types';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -43,7 +44,7 @@ function GrafanaRuleFolderExportPreview({ folder, exportFormat, onClose }: Grafa
}); });
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-rule-folder-export-preview.text-loading', 'Loading....')} />;
} }
const downloadFileName = `${folder.title}-${folder.uid}`; const downloadFileName = `${folder.title}-${folder.uid}`;
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -55,7 +56,7 @@ function GrafanaRuleGroupExportPreview({
}); });
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-rule-group-export-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoadingPlaceholder } from '@grafana/ui'; import { LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertRuleApi } from '../../api/alertRuleApi'; import { alertRuleApi } from '../../api/alertRuleApi';
@@ -40,7 +41,7 @@ function GrafanaRulesExportPreview({ exportFormat, onClose }: GrafanaRulesExport
const downloadFileName = `alert-rules-${new Date().getTime()}`; const downloadFileName = `alert-rules-${new Date().getTime()}`;
if (isFetching) { if (isFetching) {
return <LoadingPlaceholder text="Loading...." />; return <LoadingPlaceholder text={t('alerting.grafana-rules-export-preview.text-loading', 'Loading....')} />;
} }
return ( return (
@@ -13,6 +13,7 @@ import {
isTimeSeriesFrames, isTimeSeriesFrames,
} from '@grafana/data'; } from '@grafana/data';
import { Alert, AutoSizeInput, Button, IconButton, Stack, Text, clearButtonStyles, useStyles2 } from '@grafana/ui'; import { Alert, AutoSizeInput, Button, IconButton, Stack, Text, clearButtonStyles, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
import { Math } from 'app/features/expressions/components/Math'; import { Math } from 'app/features/expressions/components/Math';
import { Reduce } from 'app/features/expressions/components/Reduce'; import { Reduce } from 'app/features/expressions/components/Reduce';
@@ -164,12 +165,12 @@ export const Expression: FC<ExpressionProps> = ({
/> />
<div className={styles.expression.body}> <div className={styles.expression.body}>
{error && ( {error && (
<Alert title="Expression failed" severity="error"> <Alert title={t('alerting.expression.title-expression-failed', 'Expression failed')} severity="error">
{error.message} {error.message}
</Alert> </Alert>
)} )}
{warning && ( {warning && (
<Alert title="Expression warning" severity="warning"> <Alert title={t('alerting.expression.title-expression-warning', 'Expression warning')} severity="warning">
{warning.message} {warning.message}
</Alert> </Alert>
)} )}
@@ -246,7 +247,11 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
isRecordingRule={isRecordingRule} isRecordingRule={isRecordingRule}
/> />
))} ))}
{emptyResults && <div className={cx(styles.expression.noData, styles.mutedText)}>No data</div>} {emptyResults && (
<div className={cx(styles.expression.noData, styles.mutedText)}>
<Trans i18nKey="alerting.expression-result.no-data">No data</Trans>
</div>
)}
{shouldShowPagination && ( {shouldShowPagination && (
<div className={styles.pagination.wrapper} data-testid="paginate-expression"> <div className={styles.pagination.wrapper} data-testid="paginate-expression">
<Stack> <Stack>
@@ -256,7 +261,7 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
onClick={previousPage} onClick={previousPage}
icon="angle-left" icon="angle-left"
size="sm" size="sm"
aria-label="previous-page" aria-label={t('alerting.expression-result.aria-label-previouspage', 'previous-page')}
/> />
<Spacer /> <Spacer />
<span className={styles.mutedText}> <span className={styles.mutedText}>
@@ -269,7 +274,7 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
onClick={nextPage} onClick={nextPage}
icon="angle-right" icon="angle-right"
size="sm" size="sm"
aria-label="next-page" aria-label={t('alerting.expression-result.aria-label-nextpage', 'next-page')}
/> />
</Stack> </Stack>
</div> </div>
@@ -287,7 +292,11 @@ export const PreviewSummary: FC<{ firing: number; normal: number; isCondition: b
const { mutedText } = useStyles2(getStyles); const { mutedText } = useStyles2(getStyles);
if (seriesCount === 0) { if (seriesCount === 0) {
return <span className={mutedText}>No series</span>; return (
<span className={mutedText}>
<Trans i18nKey="alerting.preview-summary.no-series">No series</Trans>
</span>
);
} }
if (isCondition) { if (isCondition) {
@@ -478,8 +487,12 @@ const TimeseriesRow: FC<TimeseriesRowProps & { index: number }> = ({ frame, inde
<table className={styles.timeseriesTable}> <table className={styles.timeseriesTable}>
<thead> <thead>
<tr> <tr>
<th>Timestamp</th> <th>
<th>Value</th> <Trans i18nKey="alerting.timeseries-row.timestamp">Timestamp</Trans>
</th>
<th>
<Trans i18nKey="alerting.timeseries-row.value">Value</Trans>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -493,7 +506,9 @@ const TimeseriesRow: FC<TimeseriesRowProps & { index: number }> = ({ frame, inde
</table> </table>
} }
> >
<span>Time series data</span> <span>
<Trans i18nKey="alerting.timeseries-row.time-series-data">Time series data</Trans>
</span>
</PopupCard> </PopupCard>
</div> </div>
</Stack> </Stack>
@@ -3,6 +3,7 @@ import { ReactElement, useMemo, useState } from 'react';
import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data'; import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data';
import { usePluginLinks } from '@grafana/runtime'; import { usePluginLinks } from '@grafana/runtime';
import { Dropdown, IconButton } from '@grafana/ui'; import { Dropdown, IconButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal'; import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal';
// We might want to customise this in future but right now the toolbar menu from the Explore view is fine. // We might want to customise this in future but right now the toolbar menu from the Explore view is fine.
import { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu'; import { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu';
@@ -31,7 +32,11 @@ export const AlertInstanceExtensionPoint = ({
return ( return (
<> <>
<Dropdown placement="bottom-start" overlay={menu}> <Dropdown placement="bottom-start" overlay={menu}>
<IconButton name="ellipsis-v" aria-label="Actions" variant="secondary" /> <IconButton
name="ellipsis-v"
aria-label={t('alerting.alert-instance-extension-point.aria-label-actions', 'Actions')}
variant="secondary"
/>
</Dropdown> </Dropdown>
{!!selectedExtension && !!selectedExtension.path && ( {!!selectedExtension && !!selectedExtension.path && (
<ConfirmNavigationModal <ConfirmNavigationModal
@@ -52,7 +52,9 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="flex-end" wrap="wrap"> <Stack direction="row" alignItems="center" justifyContent="flex-end" wrap="wrap">
{!isGrafanaDataSource && isDisabled(muteTiming) && <Badge text="Disabled" color="orange" />} {!isGrafanaDataSource && isDisabled(muteTiming) && (
<Badge text={t('alerting.mute-timing-actions-buttons.text-disabled', 'Disabled')} color="orange" />
)}
<Authorize actions={[AlertmanagerAction.UpdateMuteTiming]}>{viewOrEditButton}</Authorize> <Authorize actions={[AlertmanagerAction.UpdateMuteTiming]}>{viewOrEditButton}</Authorize>
{exportSupported && ( {exportSupported && (
@@ -84,7 +86,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
</Stack> </Stack>
<ConfirmModal <ConfirmModal
isOpen={showDeleteDrawer} isOpen={showDeleteDrawer}
title="Delete mute timing" title={t('alerting.mute-timing-actions-buttons.title-delete-mute-timing', 'Delete mute timing')}
body={`Are you sure you would like to delete "${muteTiming.name}"?`} body={`Are you sure you would like to delete "${muteTiming.name}"?`}
confirmText={t('alerting.common.delete', 'Delete')} confirmText={t('alerting.common.delete', 'Delete')}
onConfirm={async () => { onConfirm={async () => {
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { import {
MuteTiming, MuteTiming,
useCreateMuteTiming, useCreateMuteTiming,
@@ -88,11 +88,15 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
}; };
if (loading) { if (loading) {
return <LoadingPlaceholder text="Loading mute timing" />; return <LoadingPlaceholder text={t('alerting.mute-timing-form.text-loading-mute-timing', 'Loading mute timing')} />;
} }
if (showError) { if (showError) {
return <Alert title="No matching mute timing found" />; return (
<Alert
title={t('alerting.mute-timing-form.title-no-matching-mute-timing-found', 'No matching mute timing found')}
/>
);
} }
return ( return (
@@ -103,8 +107,11 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
<FieldSet disabled={provisioned || updating}> <FieldSet disabled={provisioned || updating}>
<Field <Field
required required
label="Name" label={t('alerting.mute-timing-form.label-name', 'Name')}
description="A unique name for the mute timing" description={t(
'alerting.mute-timing-form.description-unique-timing',
'A unique name for the mute timing'
)}
invalid={!!formApi.formState.errors?.name} invalid={!!formApi.formState.errors?.name}
error={formApi.formState.errors.name?.message} error={formApi.formState.errors.name?.message}
> >
@@ -5,6 +5,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, FieldSet, Icon, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui'; import { Button, Field, FieldSet, Icon, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useAlertmanager } from '../../state/AlertmanagerContext'; import { useAlertmanager } from '../../state/AlertmanagerContext';
import { MuteTimingFields } from '../../types/mute-timing-form'; import { MuteTimingFields } from '../../types/mute-timing-form';
@@ -26,7 +27,7 @@ export const MuteTimingTimeInterval = () => {
const { isGrafanaAlertmanager } = useAlertmanager(); const { isGrafanaAlertmanager } = useAlertmanager();
return ( return (
<FieldSet label="Time intervals"> <FieldSet label={t('alerting.mute-timing-time-interval.label-time-intervals', 'Time intervals')}>
<> <>
<p> <p>
A time interval is a definition for a moment in time. All fields are lists, and at least one list element must A time interval is a definition for a moment in time. All fields are lists, and at least one list element must
@@ -46,7 +47,7 @@ export const MuteTimingTimeInterval = () => {
<div key={timeInterval.id} className={styles.timeIntervalSection}> <div key={timeInterval.id} className={styles.timeIntervalSection}>
<MuteTimingTimeRange intervalIndex={timeIntervalIndex} /> <MuteTimingTimeRange intervalIndex={timeIntervalIndex} />
<Field <Field
label="Location" label={t('alerting.mute-timing-time-interval.label-location', 'Location')}
invalid={Boolean(errors.time_intervals?.[timeIntervalIndex]?.location)} invalid={Boolean(errors.time_intervals?.[timeIntervalIndex]?.location)}
error={errors.time_intervals?.[timeIntervalIndex]?.location?.message} error={errors.time_intervals?.[timeIntervalIndex]?.location?.message}
> >
@@ -61,7 +62,7 @@ export const MuteTimingTimeInterval = () => {
data-testid="mute-timing-location" data-testid="mute-timing-location"
/> />
</Field> </Field>
<Field label="Days of the week"> <Field label={t('alerting.mute-timing-time-interval.label-days-of-the-week', 'Days of the week')}>
<DaysOfTheWeek <DaysOfTheWeek
onChange={(daysOfWeek) => { onChange={(daysOfWeek) => {
setValue(`time_intervals.${timeIntervalIndex}.weekdays`, daysOfWeek); setValue(`time_intervals.${timeIntervalIndex}.weekdays`, daysOfWeek);
@@ -71,7 +72,7 @@ export const MuteTimingTimeInterval = () => {
/> />
</Field> </Field>
<Field <Field
label="Days of the month" label={t('alerting.mute-timing-time-interval.label-days-of-the-month', 'Days of the month')}
description="The days of the month, 1:31, of a month. Negative values can be used to represent days which begin at the end of the month" description="The days of the month, 1:31, of a month. Negative values can be used to represent days which begin at the end of the month"
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.days_of_month} invalid={!!errors.time_intervals?.[timeIntervalIndex]?.days_of_month}
error={errors.time_intervals?.[timeIntervalIndex]?.days_of_month?.message} error={errors.time_intervals?.[timeIntervalIndex]?.days_of_month?.message}
@@ -83,12 +84,15 @@ export const MuteTimingTimeInterval = () => {
width={50} width={50}
// @ts-ignore react-hook-form doesn't handle nested field arrays well // @ts-ignore react-hook-form doesn't handle nested field arrays well
defaultValue={timeInterval.days_of_month} defaultValue={timeInterval.days_of_month}
placeholder="Example: 1, 14:16, -1" placeholder={t(
'alerting.mute-timing-time-interval.mute-timing-days-placeholder-example',
'Example: 1, 14:16, -1'
)}
data-testid="mute-timing-days" data-testid="mute-timing-days"
/> />
</Field> </Field>
<Field <Field
label="Months" label={t('alerting.mute-timing-time-interval.label-months', 'Months')}
description="The months of the year in either numerical or the full calendar month" description="The months of the year in either numerical or the full calendar month"
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.months} invalid={!!errors.time_intervals?.[timeIntervalIndex]?.months}
error={errors.time_intervals?.[timeIntervalIndex]?.months?.message} error={errors.time_intervals?.[timeIntervalIndex]?.months?.message}
@@ -103,14 +107,17 @@ export const MuteTimingTimeInterval = () => {
), ),
})} })}
width={50} width={50}
placeholder="Example: 1:3, may:august, december" placeholder={t(
'alerting.mute-timing-time-interval.mute-timing-months-placeholder-example-mayaugust-december',
'Example: 1:3, may:august, december'
)}
// @ts-ignore react-hook-form doesn't handle nested field arrays well // @ts-ignore react-hook-form doesn't handle nested field arrays well
defaultValue={timeInterval.months} defaultValue={timeInterval.months}
data-testid="mute-timing-months" data-testid="mute-timing-months"
/> />
</Field> </Field>
<Field <Field
label="Years" label={t('alerting.mute-timing-time-interval.label-years', 'Years')}
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.years} invalid={!!errors.time_intervals?.[timeIntervalIndex]?.years}
error={errors.time_intervals?.[timeIntervalIndex]?.years?.message ?? ''} error={errors.time_intervals?.[timeIntervalIndex]?.years?.message ?? ''}
> >
@@ -119,7 +126,10 @@ export const MuteTimingTimeInterval = () => {
validate: (value) => validateArrayField(value, (year) => /^\d{4}$/.test(year), 'Invalid year'), validate: (value) => validateArrayField(value, (year) => /^\d{4}$/.test(year), 'Invalid year'),
})} })}
width={50} width={50}
placeholder="Example: 2021:2022, 2030" placeholder={t(
'alerting.mute-timing-time-interval.mute-timing-years-placeholder-example',
'Example: 2021:2022, 2030'
)}
// @ts-ignore react-hook-form doesn't handle nested field arrays well // @ts-ignore react-hook-form doesn't handle nested field arrays well
defaultValue={timeInterval.years} defaultValue={timeInterval.years}
data-testid="mute-timing-years" data-testid="mute-timing-years"
@@ -133,7 +143,9 @@ export const MuteTimingTimeInterval = () => {
icon="trash-alt" icon="trash-alt"
onClick={() => removeTimeInterval(timeIntervalIndex)} onClick={() => removeTimeInterval(timeIntervalIndex)}
> >
Remove time interval <Trans i18nKey="alerting.mute-timing-time-interval.remove-time-interval">
Remove time interval
</Trans>
</Button> </Button>
{/* {/*
This switch is only available for Grafana Alertmanager, as for now, Grafana alert manager doesn't support this feature This switch is only available for Grafana Alertmanager, as for now, Grafana alert manager doesn't support this feature
@@ -142,7 +154,7 @@ export const MuteTimingTimeInterval = () => {
{!isGrafanaAlertmanager && ( {!isGrafanaAlertmanager && (
<InlineSwitch <InlineSwitch
id={`time_intervals.${timeIntervalIndex}.disable`} id={`time_intervals.${timeIntervalIndex}.disable`}
label="Disable" label={t('alerting.mute-timing-time-interval.label-disable', 'Disable')}
showLabel showLabel
transparent transparent
{...register(`time_intervals.${timeIntervalIndex}.disable`)} {...register(`time_intervals.${timeIntervalIndex}.disable`)}
@@ -162,7 +174,9 @@ export const MuteTimingTimeInterval = () => {
}} }}
icon="plus" icon="plus"
> >
Add another time interval <Trans i18nKey="alerting.mute-timing-time-interval.add-another-time-interval">
Add another time interval
</Trans>
</Button> </Button>
</> </>
</FieldSet> </FieldSet>
@@ -3,6 +3,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, Icon, IconButton, InlineField, InlineFieldRow, Input, Tooltip, useStyles2 } from '@grafana/ui'; import { Button, Field, Icon, IconButton, InlineField, InlineFieldRow, Input, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { MuteTimingFields } from '../../types/mute-timing-form'; import { MuteTimingFields } from '../../types/mute-timing-form';
import ConditionalWrap from '../ConditionalWrap'; import ConditionalWrap from '../ConditionalWrap';
@@ -35,7 +36,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
<div> <div>
<Field <Field
className={styles.field} className={styles.field}
label="Time range" label={t('alerting.mute-timing-time-range.label-time-range', 'Time range')}
description="The time inclusive of the start and exclusive of the end time (in UTC if no location has been selected, otherwise local time)" description="The time inclusive of the start and exclusive of the end time (in UTC if no location has been selected, otherwise local time)"
invalid={timeRangeInvalid} invalid={timeRangeInvalid}
> >
@@ -58,7 +59,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
<div className={styles.timeRange} key={timeRange.id}> <div className={styles.timeRange} key={timeRange.id}>
<InlineFieldRow> <InlineFieldRow>
<InlineField <InlineField
label="Start time" label={t('alerting.mute-timing-time-range.label-start-time', 'Start time')}
invalid={Boolean(timeRangeErrors?.start_time)} invalid={Boolean(timeRangeErrors?.start_time)}
error={timeRangeErrors?.start_time?.message} error={timeRangeErrors?.start_time?.message}
> >
@@ -86,12 +87,12 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
suffix={<Icon name="clock-nine" />} suffix={<Icon name="clock-nine" />}
// @ts-ignore react-hook-form doesn't handle nested field arrays well // @ts-ignore react-hook-form doesn't handle nested field arrays well
defaultValue={timeRange.start_time} defaultValue={timeRange.start_time}
placeholder="HH:mm" placeholder={t('alerting.mute-timing-time-range.mute-timing-starts-at-placeholder-hhmm', 'HH:mm')}
data-testid="mute-timing-starts-at" data-testid="mute-timing-starts-at"
/> />
</InlineField> </InlineField>
<InlineField <InlineField
label="End time" label={t('alerting.mute-timing-time-range.label-end-time', 'End time')}
invalid={Boolean(timeRangeErrors?.end_time)} invalid={Boolean(timeRangeErrors?.end_time)}
error={timeRangeErrors?.end_time?.message} error={timeRangeErrors?.end_time?.message}
> >
@@ -118,19 +119,19 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
suffix={<Icon name="clock-nine" />} suffix={<Icon name="clock-nine" />}
// @ts-ignore react-hook-form doesn't handle nested field arrays well // @ts-ignore react-hook-form doesn't handle nested field arrays well
defaultValue={timeRange.end_time} defaultValue={timeRange.end_time}
placeholder="HH:mm" placeholder={t('alerting.mute-timing-time-range.mute-timing-ends-at-placeholder-hhmm', 'HH:mm')}
data-testid="mute-timing-ends-at" data-testid="mute-timing-ends-at"
/> />
</InlineField> </InlineField>
<IconButton <IconButton
className={styles.deleteTimeRange} className={styles.deleteTimeRange}
title="Remove" title={t('alerting.mute-timing-time-range.title-remove', 'Remove')}
name="trash-alt" name="trash-alt"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
removeTimeRange(index); removeTimeRange(index);
}} }}
tooltip="Remove time range" tooltip={t('alerting.mute-timing-time-range.tooltip-remove-time-range', 'Remove time range')}
/> />
</InlineFieldRow> </InlineFieldRow>
</div> </div>
@@ -154,7 +155,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
disabled={isDisabled} disabled={isDisabled}
onClick={() => addTimeRange({ start_time: '', end_time: '' })} onClick={() => addTimeRange({ start_time: '', end_time: '' })}
> >
Add another time range <Trans i18nKey="alerting.mute-timing-time-range.add-another-time-range">Add another time range</Trans>
</Button> </Button>
</ConditionalWrap> </ConditionalWrap>
</div> </div>
@@ -55,7 +55,11 @@ export const MuteTimingsTable = () => {
const columns = useColumns(alertManagerSourceName, hideActions); const columns = useColumns(alertManagerSourceName, hideActions);
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading mute timings..." />; return (
<LoadingPlaceholder
text={t('alerting.mute-timings-table.text-loading-mute-timings', 'Loading mute timings...')}
/>
);
} }
if (error) { if (error) {
@@ -107,7 +111,10 @@ export const MuteTimingsTable = () => {
<DynamicTable items={items} cols={columns} pagination={{ itemsPerPage: 25 }} /> <DynamicTable items={items} cols={columns} pagination={{ itemsPerPage: 25 }} />
) : !hideActions ? ( ) : !hideActions ? (
<EmptyAreaWithCTA <EmptyAreaWithCTA
text="You haven't created any mute timings yet" text={t(
'alerting.mute-timings-table.text-havent-created-timings',
"You haven't created any mute timings yet"
)}
buttonLabel="Add mute timing" buttonLabel="Add mute timing"
buttonIcon="plus" buttonIcon="plus"
buttonSize="lg" buttonSize="lg"
@@ -115,7 +122,11 @@ export const MuteTimingsTable = () => {
showButton={allowedToCreateMuteTiming} showButton={allowedToCreateMuteTiming}
/> />
) : ( ) : (
<EmptyAreaWithCTA text="No mute timings configured" buttonLabel={''} showButton={false} /> <EmptyAreaWithCTA
text={t('alerting.mute-timings-table.text-no-mute-timings-configured', 'No mute timings configured')}
buttonLabel={''}
showButton={false}
/>
)} )}
</div> </div>
); );
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Alert, IconButton, Select, SelectCommonProps, Stack, Text, useStyles2 } from '@grafana/ui'; import { Alert, IconButton, Select, SelectCommonProps, Stack, Text, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ContactPointReceiverSummary } from 'app/features/alerting/unified/components/contact-points/ContactPoint'; import { ContactPointReceiverSummary } from 'app/features/alerting/unified/components/contact-points/ContactPoint';
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
@@ -69,7 +70,15 @@ export const ContactPointSelector = ({
// TODO error handling // TODO error handling
if (error) { if (error) {
return <Alert title="Failed to fetch contact points" severity="error" />; return (
<Alert
title={t(
'alerting.contact-point-selector.title-failed-to-fetch-contact-points',
'Failed to fetch contact points'
)}
severity="error"
/>
);
} }
return ( return (
@@ -86,8 +95,11 @@ export const ContactPointSelector = ({
<IconButton <IconButton
name="sync" name="sync"
onClick={onClickRefresh} onClick={onClickRefresh}
aria-label="Refresh contact points" aria-label={t('alerting.contact-point-selector.aria-label-refresh-contact-points', 'Refresh contact points')}
tooltip="Refresh contact points list" tooltip={t(
'alerting.contact-point-selector.tooltip-refresh-contact-points-list',
'Refresh contact points list'
)}
className={cx(styles.refreshButton, { className={cx(styles.refreshButton, {
[styles.loading]: loaderSpinning || isLoading, [styles.loading]: loaderSpinning || isLoading,
})} })}
@@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { Collapse, Field, Link, MultiSelect, useStyles2 } from '@grafana/ui'; import { Collapse, Field, Link, MultiSelect, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils'; import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils';
import { RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { RouteWithID } from 'app/plugins/datasource/alertmanager/types';
@@ -52,7 +53,7 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Field <Field
label="Default contact point" label={t('alerting.am-root-route-form.label-default-contact-point', 'Default contact point')}
invalid={Boolean(errors.receiver) ? true : undefined} invalid={Boolean(errors.receiver) ? true : undefined}
error={errors.receiver?.message} error={errors.receiver?.message}
> >
@@ -71,24 +72,26 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
name="receiver" name="receiver"
rules={{ required: { value: true, message: 'Required.' } }} rules={{ required: { value: true, message: 'Required.' } }}
/> />
<span>or</span> <span>
<Trans i18nKey="alerting.am-root-route-form.or">or</Trans>
</span>
<Link <Link
className={styles.linkText} className={styles.linkText}
href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)} href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}
> >
Create a contact point <Trans i18nKey="alerting.am-root-route-form.create-a-contact-point">Create a contact point</Trans>
</Link> </Link>
</div> </div>
</Field> </Field>
<Field <Field
label="Group by" label={t('alerting.am-root-route-form.am-group-select-label-group-by', 'Group by')}
description="Combine multiple alerts into a single notification by grouping them by the same label values." description="Combine multiple alerts into a single notification by grouping them by the same label values."
data-testid="am-group-select" data-testid="am-group-select"
> >
<Controller <Controller
render={({ field: { onChange, ref, ...field } }) => ( render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect <MultiSelect
aria-label="Group by" aria-label={t('alerting.am-root-route-form.aria-label-group-by', 'Group by')}
{...field} {...field}
allowCustomValue allowCustomValue
className={styles.input} className={styles.input}
@@ -108,12 +111,12 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
collapsible collapsible
className={styles.collapse} className={styles.collapse}
isOpen={isTimingOptionsExpanded} isOpen={isTimingOptionsExpanded}
label="Timing options" label={t('alerting.am-root-route-form.label-timing-options', 'Timing options')}
onToggle={setIsTimingOptionsExpanded} onToggle={setIsTimingOptionsExpanded}
> >
<div className={styles.timingFormContainer}> <div className={styles.timingFormContainer}>
<Field <Field
label="Group wait" label={t('alerting.am-root-route-form.am-group-wait-label-group-wait', 'Group wait')}
description="The waiting time before sending the first notification for a new group of alerts. Default 30 seconds." description="The waiting time before sending the first notification for a new group of alerts. Default 30 seconds."
invalid={!!errors.groupWaitValue} invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message} error={errors.groupWaitValue?.message}
@@ -123,11 +126,11 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
{...register('groupWaitValue', { validate: promDurationValidator })} {...register('groupWaitValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_wait} placeholder={TIMING_OPTIONS_DEFAULTS.group_wait}
className={styles.promDurationInput} className={styles.promDurationInput}
aria-label="Group wait" aria-label={t('alerting.am-root-route-form.aria-label-group-wait', 'Group wait')}
/> />
</Field> </Field>
<Field <Field
label="Group interval" label={t('alerting.am-root-route-form.am-group-interval-label-group-interval', 'Group interval')}
description="The wait time before sending a notification about changes in the alert group after the first notification has been sent. Default is 5 minutes." description="The wait time before sending a notification about changes in the alert group after the first notification has been sent. Default is 5 minutes."
invalid={!!errors.groupIntervalValue} invalid={!!errors.groupIntervalValue}
error={errors.groupIntervalValue?.message} error={errors.groupIntervalValue?.message}
@@ -137,11 +140,11 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
{...register('groupIntervalValue', { validate: promDurationValidator })} {...register('groupIntervalValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_interval} placeholder={TIMING_OPTIONS_DEFAULTS.group_interval}
className={styles.promDurationInput} className={styles.promDurationInput}
aria-label="Group interval" aria-label={t('alerting.am-root-route-form.aria-label-group-interval', 'Group interval')}
/> />
</Field> </Field>
<Field <Field
label="Repeat interval" label={t('alerting.am-root-route-form.am-repeat-interval-label-repeat-interval', 'Repeat interval')}
description="The wait time before resending a notification that has already been sent successfully. Default is 4 hours. Should be a multiple of Group interval." description="The wait time before resending a notification that has already been sent successfully. Default is 4 hours. Should be a multiple of Group interval."
invalid={!!errors.repeatIntervalValue} invalid={!!errors.repeatIntervalValue}
error={errors.repeatIntervalValue?.message} error={errors.repeatIntervalValue?.message}
@@ -156,7 +159,7 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
})} })}
placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval} placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval}
className={styles.promDurationInput} className={styles.promDurationInput}
aria-label="Repeat interval" aria-label={t('alerting.am-root-route-form.aria-label-repeat-interval', 'Repeat interval')}
/> />
</Field> </Field>
</div> </div>
@@ -16,6 +16,7 @@ import {
Switch, Switch,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector'; import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector';
import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils'; import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils';
@@ -86,7 +87,9 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<input type="hidden" {...register('id')} /> <input type="hidden" {...register('id')} />
<Stack direction="column" alignItems="flex-start"> <Stack direction="column" alignItems="flex-start">
<div>Matching labels</div> <div>
<Trans i18nKey="alerting.am-routes-expanded-form.matching-labels">Matching labels</Trans>
</div>
{fields.length === 0 && ( {fields.length === 0 && (
<Badge <Badge
color="orange" color="orange"
@@ -101,14 +104,14 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
return ( return (
<Stack direction="row" key={field.id} alignItems="center"> <Stack direction="row" key={field.id} alignItems="center">
<Field <Field
label="Label" label={t('alerting.am-routes-expanded-form.label-label', 'Label')}
invalid={!!errors.object_matchers?.[index]?.name} invalid={!!errors.object_matchers?.[index]?.name}
error={errors.object_matchers?.[index]?.name?.message} error={errors.object_matchers?.[index]?.name?.message}
> >
<Input <Input
{...register(`object_matchers.${index}.name`, { required: 'Field is required' })} {...register(`object_matchers.${index}.name`, { required: 'Field is required' })}
defaultValue={field.name} defaultValue={field.name}
placeholder="label" placeholder={t('alerting.am-routes-expanded-form.placeholder-label', 'label')}
autoFocus autoFocus
/> />
</Field> </Field>
@@ -120,7 +123,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
className={styles.matchersOperator} className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)} onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions} options={matcherFieldOptions}
aria-label="Operator" aria-label={t('alerting.am-routes-expanded-form.aria-label-operator', 'Operator')}
/> />
)} )}
defaultValue={field.operator} defaultValue={field.operator}
@@ -130,18 +133,22 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
/> />
</Field> </Field>
<Field <Field
label="Value" label={t('alerting.am-routes-expanded-form.label-value', 'Value')}
invalid={!!errors.object_matchers?.[index]?.value} invalid={!!errors.object_matchers?.[index]?.value}
error={errors.object_matchers?.[index]?.value?.message} error={errors.object_matchers?.[index]?.value?.message}
> >
<Input <Input
{...register(`object_matchers.${index}.value`)} {...register(`object_matchers.${index}.value`)}
defaultValue={field.value} defaultValue={field.value}
placeholder="value" placeholder={t('alerting.am-routes-expanded-form.placeholder-value', 'value')}
/> />
</Field> </Field>
<IconButton tooltip="Remove matcher" name={'trash-alt'} onClick={() => remove(index)}> <IconButton
Remove tooltip={t('alerting.am-routes-expanded-form.tooltip-remove-matcher', 'Remove matcher')}
name={'trash-alt'}
onClick={() => remove(index)}
>
<Trans i18nKey="alerting.am-routes-expanded-form.remove">Remove</Trans>
</IconButton> </IconButton>
</Stack> </Stack>
); );
@@ -155,11 +162,11 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
variant="secondary" variant="secondary"
type="button" type="button"
> >
Add matcher <Trans i18nKey="alerting.am-routes-expanded-form.add-matcher">Add matcher</Trans>
</Button> </Button>
</Stack> </Stack>
<Field label="Contact point"> <Field label={t('alerting.am-routes-expanded-form.label-contact-point', 'Contact point')}>
<Controller <Controller
render={({ field: { onChange, ref, value, ...field } }) => ( render={({ field: { onChange, ref, value, ...field } }) => (
<ContactPointSelector <ContactPointSelector
@@ -176,15 +183,20 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
name="receiver" name="receiver"
/> />
</Field> </Field>
<Field label="Continue matching subsequent sibling nodes"> <Field
label={t(
'alerting.am-routes-expanded-form.label-continue-matching-subsequent-sibling-nodes',
'Continue matching subsequent sibling nodes'
)}
>
<Switch id="continue-toggle" {...register('continue')} /> <Switch id="continue-toggle" {...register('continue')} />
</Field> </Field>
<Field label="Override grouping"> <Field label={t('alerting.am-routes-expanded-form.label-override-grouping', 'Override grouping')}>
<Switch id="override-grouping-toggle" {...register('overrideGrouping')} /> <Switch id="override-grouping-toggle" {...register('overrideGrouping')} />
</Field> </Field>
{watch().overrideGrouping && ( {watch().overrideGrouping && (
<Field <Field
label="Group by" label={t('alerting.am-routes-expanded-form.label-group-by', 'Group by')}
description="Combine multiple alerts into a single notification by grouping them by the same label values. If empty, it is inherited from the parent policy." description="Combine multiple alerts into a single notification by grouping them by the same label values. If empty, it is inherited from the parent policy."
> >
<Controller <Controller
@@ -199,7 +211,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
<> <>
<MultiSelect <MultiSelect
aria-label="Group by" aria-label={t('alerting.am-routes-expanded-form.aria-label-group-by', 'Group by')}
{...field} {...field}
invalid={Boolean(error)} invalid={Boolean(error)}
allowCustomValue allowCustomValue
@@ -219,7 +231,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
/> />
</Field> </Field>
)} )}
<Field label="Override general timings"> <Field label={t('alerting.am-routes-expanded-form.label-override-general-timings', 'Override general timings')}>
<Switch id="override-timings-toggle" {...register('overrideTimings')} /> <Switch id="override-timings-toggle" {...register('overrideTimings')} />
</Field> </Field>
{watch().overrideTimings && ( {watch().overrideTimings && (
@@ -268,9 +280,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
</> </>
)} )}
<Field <Field
label="Mute timings" label={t('alerting.am-routes-expanded-form.am-mute-timing-select-label-mute-timings', 'Mute timings')}
data-testid="am-mute-timing-select" data-testid="am-mute-timing-select"
description="Add mute timing to policy" description={t(
'alerting.am-routes-expanded-form.am-mute-timing-select-description-add-mute-timing-to-policy',
'Add mute timing to policy'
)}
invalid={!!errors.muteTimeIntervals} invalid={!!errors.muteTimeIntervals}
> >
<Controller <Controller
@@ -3,7 +3,7 @@ import { debounce, isEqual } from 'lodash';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; import { Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
@@ -92,7 +92,7 @@ const NotificationPoliciesFilter = ({
<Input <Input
ref={searchInputRef} ref={searchInputRef}
data-testid="search-query-input" data-testid="search-query-input"
placeholder="Search" placeholder={t('alerting.notification-policies-filter.search-query-input-placeholder-search', 'Search')}
width={46} width={46}
prefix={<Icon name="search" />} prefix={<Icon name="search" />}
onChange={(event) => { onChange={(event) => {
@@ -102,7 +102,10 @@ const NotificationPoliciesFilter = ({
/> />
</Field> </Field>
{contactPointsSupported && canSeeContactPoints && ( {contactPointsSupported && canSeeContactPoints && (
<Field label="Search by contact point" style={{ marginBottom: 0 }}> <Field
label={t('alerting.notification-policies-filter.label-search-by-contact-point', 'Search by contact point')}
style={{ marginBottom: 0 }}
>
<ContactPointSelector <ContactPointSelector
selectProps={{ selectProps={{
id: 'receiver', id: 'receiver',
@@ -2,7 +2,7 @@ import { groupBy } from 'lodash';
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { Button, Icon, Modal, ModalProps, Spinner, Stack } from '@grafana/ui'; import { Button, Icon, Modal, ModalProps, Spinner, Stack } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { AlertState, AlertmanagerGroup, ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { AlertState, AlertmanagerGroup, ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute } from '../../types/amroutes'; import { FormAmRoute } from '../../types/amroutes';
@@ -52,7 +52,10 @@ const useAddPolicyModal = (
onDismiss={handleDismiss} onDismiss={handleDismiss}
closeOnBackdropClick={true} closeOnBackdropClick={true}
closeOnEscape={true} closeOnEscape={true}
title="Add notification policy" title={t(
'alerting.use-add-policy-modal.modal-element.title-add-notification-policy',
'Add notification policy'
)}
> >
{error && <NotificationPoliciesErrorAlert error={error} />} {error && <NotificationPoliciesErrorAlert error={error} />}
<AmRoutesExpandedForm <AmRoutesExpandedForm
@@ -116,7 +119,10 @@ const useEditPolicyModal = (
onDismiss={handleDismiss} onDismiss={handleDismiss}
closeOnBackdropClick={true} closeOnBackdropClick={true}
closeOnEscape={true} closeOnEscape={true}
title="Edit notification policy" title={t(
'alerting.use-edit-policy-modal.modal-element.title-edit-notification-policy',
'Edit notification policy'
)}
> >
{error && <NotificationPoliciesErrorAlert error={error} />} {error && <NotificationPoliciesErrorAlert error={error} />}
{isDefaultPolicy && route && ( {isDefaultPolicy && route && (
@@ -191,7 +197,10 @@ const useDeletePolicyModal = (
onDismiss={handleDismiss} onDismiss={handleDismiss}
closeOnBackdropClick={true} closeOnBackdropClick={true}
closeOnEscape={true} closeOnEscape={true}
title="Delete notification policy" title={t(
'alerting.use-delete-policy-modal.modal-element.title-delete-notification-policy',
'Delete notification policy'
)}
> >
{error && <NotificationPoliciesErrorAlert error={error} />} {error && <NotificationPoliciesErrorAlert error={error} />}
<Trans i18nKey="alerting.policies.delete.warning-1"> <Trans i18nKey="alerting.policies.delete.warning-1">
@@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
import { Alert, Button, Stack } from '@grafana/ui'; import { Alert, Button, Stack } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { useContactPointsWithStatus } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useContactPointsWithStatus } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes'; import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes';
@@ -201,13 +201,25 @@ export const NotificationPoliciesList = () => {
return ( return (
<> <>
{hasPoliciesError && ( {hasPoliciesError && (
<Alert severity="error" title="Error loading Alertmanager config"> <Alert
severity="error"
title={t(
'alerting.notification-policies-list.title-error-loading-alertmanager-config',
'Error loading Alertmanager config'
)}
>
{stringifyErrorLike(fetchPoliciesError) || 'Unknown error.'} {stringifyErrorLike(fetchPoliciesError) || 'Unknown error.'}
</Alert> </Alert>
)} )}
{/* show when there is an update error */} {/* show when there is an update error */}
{hasConflictError && ( {hasConflictError && (
<Alert severity="info" title="Notification policies have changed"> <Alert
severity="info"
title={t(
'alerting.notification-policies-list.title-notification-policies-have-changed',
'Notification policies have changed'
)}
>
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack direction="row" justifyContent="space-between" alignItems="center">
<Trans i18nKey="alerting.policies.update-errors.conflict"> <Trans i18nKey="alerting.policies.update-errors.conflict">
The notification policy tree has been updated by another user. The notification policy tree has been updated by another user.
@@ -270,18 +270,18 @@ const Policy = (props: PolicyComponentProps) => {
overlay={ overlay={
<Menu> <Menu>
<Menu.Item <Menu.Item
label="New sibling above" label={t('alerting.policy.label-new-sibling-above', 'New sibling above')}
icon="arrow-up" icon="arrow-up"
onClick={() => onAddPolicy(currentRoute, 'above')} onClick={() => onAddPolicy(currentRoute, 'above')}
/> />
<Menu.Item <Menu.Item
label="New sibling below" label={t('alerting.policy.label-new-sibling-below', 'New sibling below')}
icon="arrow-down" icon="arrow-down"
onClick={() => onAddPolicy(currentRoute, 'below')} onClick={() => onAddPolicy(currentRoute, 'below')}
/> />
<Menu.Divider /> <Menu.Divider />
<Menu.Item <Menu.Item
label="New child policy" label={t('alerting.policy.label-new-child-policy', 'New child policy')}
icon="plus" icon="plus"
onClick={() => onAddPolicy(currentRoute, 'child')} onClick={() => onAddPolicy(currentRoute, 'child')}
/> />
@@ -553,7 +553,7 @@ export const useCreateDropdownMenuActions = (
<Menu.Item <Menu.Item
icon="edit" icon="edit"
disabled={provisioned || isAutoGenerated} disabled={provisioned || isAutoGenerated}
label="Edit" label={t('alerting.use-create-dropdown-menu-actions.label-edit', 'Edit')}
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)} onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
/> />
</ConditionalWrap> </ConditionalWrap>
@@ -563,7 +563,12 @@ export const useCreateDropdownMenuActions = (
if (showExportAction) { if (showExportAction) {
dropdownMenuActions.push( dropdownMenuActions.push(
<Menu.Item key="export-policy" icon="download-alt" label="Export" onClick={toggleShowExportDrawer} /> <Menu.Item
key="export-policy"
icon="download-alt"
label={t('alerting.use-create-dropdown-menu-actions.label-export', 'Export')}
onClick={toggleShowExportDrawer}
/>
); );
} }
@@ -576,7 +581,7 @@ export const useCreateDropdownMenuActions = (
destructive destructive
icon="trash-alt" icon="trash-alt"
disabled={provisioned || isAutoGenerated} disabled={provisioned || isAutoGenerated}
label="Delete" label={t('alerting.use-create-dropdown-menu-actions.label-delete', 'Delete')}
onClick={() => onDeletePolicy(currentRoute)} onClick={() => onDeletePolicy(currentRoute)}
/> />
</ConditionalWrap> </ConditionalWrap>
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { TimeOptions } from '../../types/time'; import { TimeOptions } from '../../types/time';
@@ -15,9 +16,15 @@ export function PromDurationDocs() {
<hr /> <hr />
<div className={styles.list}> <div className={styles.list}>
<div className={styles.header}> <div className={styles.header}>
<div>Symbol</div> <div>
<div>Time unit</div> <Trans i18nKey="alerting.prom-duration-docs.symbol">Symbol</Trans>
<div>Example</div> </div>
<div>
<Trans i18nKey="alerting.prom-duration-docs.time-unit">Time unit</Trans>
</div>
<div>
<Trans i18nKey="alerting.prom-duration-docs.example">Example</Trans>
</div>
</div> </div>
<PromDurationDocsTimeUnit unit={TimeOptions.seconds} name="seconds" example="20s" /> <PromDurationDocsTimeUnit unit={TimeOptions.seconds} name="seconds" example="20s" />
<PromDurationDocsTimeUnit unit={TimeOptions.minutes} name="minutes" example="10m" /> <PromDurationDocsTimeUnit unit={TimeOptions.minutes} name="minutes" example="10m" />
@@ -25,7 +32,10 @@ export function PromDurationDocs() {
<PromDurationDocsTimeUnit unit={TimeOptions.days} name="days" example="3d" /> <PromDurationDocsTimeUnit unit={TimeOptions.days} name="days" example="3d" />
<PromDurationDocsTimeUnit unit={TimeOptions.weeks} name="weeks" example="2w" /> <PromDurationDocsTimeUnit unit={TimeOptions.weeks} name="weeks" example="2w" />
<div className={styles.examples}> <div className={styles.examples}>
<div>Multiple units combined</div> <div>
<Trans i18nKey="alerting.prom-duration-docs.multiple-units-combined">Multiple units combined</Trans>
</div>
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<code>1m30s, 2h30m20s, 1w2d</code> <code>1m30s, 2h30m20s, 1w2d</code>
</div> </div>
</div> </div>
@@ -3,6 +3,7 @@ import { useAsync } from 'react-use';
import { urlUtil } from '@grafana/data'; import { urlUtil } from '@grafana/data';
import { Alert, Button, LinkButton } from '@grafana/ui'; import { Alert, Button, LinkButton } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
@@ -30,12 +31,22 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
); );
if (loading) { if (loading) {
return <Button disabled={true}>New alert rule</Button>; return (
<Button disabled={true}>
<Trans i18nKey="alerting.new-rule-from-panel-button.new-alert-rule">New alert rule</Trans>
</Button>
);
} }
if (!formValues) { if (!formValues) {
return ( return (
<Alert severity="info" title="No alerting capable query found"> <Alert
severity="info"
title={t(
'alerting.new-rule-from-panel-button.title-no-alerting-capable-query-found',
'No alerting capable query found'
)}
>
Cannot create alerts from this panel because no query to an alerting capable datasource is found. Cannot create alerts from this panel because no query to an alerting capable datasource is found.
</Alert> </Alert>
); );
@@ -54,7 +65,7 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
className={className} className={className}
data-testid="create-alert-rule-button" data-testid="create-alert-rule-button"
> >
New alert rule <Trans i18nKey="alerting.new-rule-from-panel-button.new-alert-rule">New alert rule</Trans>
</LinkButton> </LinkButton>
); );
}; };
@@ -15,7 +15,7 @@ import {
clearButtonStyles, clearButtonStyles,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { AlertmanagerAlert, TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert, TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../../api/alertmanagerApi'; import { alertmanagerApi } from '../../api/alertmanagerApi';
@@ -208,7 +208,7 @@ export function AlertInstanceModalSelector({
return ( return (
<div> <div>
<Modal <Modal
title="Select alert instances" title={t('alerting.alert-instance-modal-selector.title-select-alert-instances', 'Select alert instances')}
className={styles.modal} className={styles.modal}
closeOnEscape closeOnEscape
isOpen={isOpen} isOpen={isOpen}
@@ -219,14 +219,19 @@ export function AlertInstanceModalSelector({
<FilterInput <FilterInput
value={ruleFilter} value={ruleFilter}
onChange={handleSearchRules} onChange={handleSearchRules}
title="Search alert rule" title={t('alerting.alert-instance-modal-selector.title-search-alert-rule', 'Search alert rule')}
placeholder="Search alert rule" placeholder={t('alerting.alert-instance-modal-selector.placeholder-search-alert-rule', 'Search alert rule')}
autoFocus autoFocus
/> />
<div>{(selectedRule && 'Select one or more instances from the list below') || ''}</div> <div>{(selectedRule && 'Select one or more instances from the list below') || ''}</div>
<div className={styles.column}> <div className={styles.column}>
{loading && <LoadingPlaceholder text="Loading rules..." className={styles.loadingPlaceholder} />} {loading && (
<LoadingPlaceholder
text={t('alerting.alert-instance-modal-selector.text-loading-rules', 'Loading rules...')}
className={styles.loadingPlaceholder}
/>
)}
{!loading && ( {!loading && (
<AutoSizer> <AutoSizer>
@@ -245,7 +250,12 @@ export function AlertInstanceModalSelector({
<div>Select an alert rule to get a list of available firing instances</div> <div>Select an alert rule to get a list of available firing instances</div>
</div> </div>
)} )}
{loading && <LoadingPlaceholder text="Loading rule..." className={styles.loadingPlaceholder} />} {loading && (
<LoadingPlaceholder
text={t('alerting.alert-instance-modal-selector.text-loading-rule', 'Loading rule...')}
className={styles.loadingPlaceholder}
/>
)}
{selectedRule && rulesWithInstances[selectedRule].length && !loading && ( {selectedRule && rulesWithInstances[selectedRule].length && !loading && (
<AutoSizer> <AutoSizer>
@@ -277,7 +287,9 @@ export function AlertInstanceModalSelector({
} }
}} }}
> >
Add alert data to payload <Trans i18nKey="alerting.alert-instance-modal-selector.add-alert-data-to-payload">
Add alert data to payload
</Trans>
</Button> </Button>
</Modal.ButtonRow> </Modal.ButtonRow>
</Modal> </Modal>
@@ -2,7 +2,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { Alert, Button, LinkButton, Stack } from '@grafana/ui'; import { Alert, Button, LinkButton, Stack } from '@grafana/ui';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
@@ -72,7 +72,10 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => {
<FormProvider {...formAPI}> <FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmitCallback)}> <form onSubmit={handleSubmit(onSubmitCallback)}>
{error && ( {error && (
<Alert severity="error" title="Error saving receiver"> <Alert
severity="error"
title={t('alerting.global-config-form.title-error-saving-receiver', 'Error saving receiver')}
>
{error.message || String(error)} {error.message || String(error)}
</Alert> </Alert>
)} )}
@@ -92,10 +95,14 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => {
<> <>
{loading && ( {loading && (
<Button disabled={true} icon="spinner" variant="primary"> <Button disabled={true} icon="spinner" variant="primary">
Saving... <Trans i18nKey="alerting.global-config-form.saving">Saving...</Trans>
</Button>
)}
{!loading && (
<Button type="submit">
<Trans i18nKey="alerting.global-config-form.save-global-config">Save global config</Trans>
</Button> </Button>
)} )}
{!loading && <Button type="submit">Save global config</Button>}
</> </>
)} )}
<LinkButton <LinkButton
@@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, CodeEditor, Dropdown, Menu, Stack, Toggletip, useStyles2 } from '@grafana/ui'; import { Button, CodeEditor, Dropdown, Menu, Stack, Toggletip, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
@@ -84,19 +85,22 @@ export function PayloadEditor({
<> <>
<div className={cx(styles.wrapper, className)}> <div className={cx(styles.wrapper, className)}>
<EditorColumnHeader <EditorColumnHeader
label="Payload" label={t('alerting.payload-editor.label-payload', 'Payload')}
actions={ actions={
<Stack direction="row" alignItems="center" gap={0.5}> <Stack direction="row" alignItems="center" gap={0.5}>
<Dropdown <Dropdown
overlay={ overlay={
<Menu> <Menu>
<Menu.Item <Menu.Item
label="Use existing alert instances" label={t(
'alerting.payload-editor.label-use-existing-alert-instances',
'Use existing alert instances'
)}
disabled={errorInPayloadJson} disabled={errorInPayloadJson}
onClick={onOpenAlertSelectorModal} onClick={onOpenAlertSelectorModal}
/> />
<Menu.Item <Menu.Item
label="Add custom alert instance" label={t('alerting.payload-editor.label-add-custom-alert-instance', 'Add custom alert instance')}
disabled={errorInPayloadJson} disabled={errorInPayloadJson}
onClick={onOpenEditAlertModal} onClick={onOpenEditAlertModal}
/> />
@@ -106,12 +110,12 @@ export function PayloadEditor({
} }
> >
<Button variant="secondary" size="sm" icon="angle-down"> <Button variant="secondary" size="sm" icon="angle-down">
Edit payload <Trans i18nKey="alerting.payload-editor.edit-payload">Edit payload</Trans>
</Button> </Button>
</Dropdown> </Dropdown>
<Toggletip content={<AlertTemplateDataTable />} placement="top" fitContent> <Toggletip content={<AlertTemplateDataTable />} placement="top" fitContent>
<Button variant="secondary" fill="outline" size="sm" icon="question-circle"> <Button variant="secondary" fill="outline" size="sm" icon="question-circle">
Reference <Trans i18nKey="alerting.payload-editor.reference">Reference</Trans>
</Button> </Button>
</Toggletip> </Toggletip>
</Stack> </Stack>
@@ -5,6 +5,7 @@ import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Dropdown, Icon, Menu, MenuItem, Stack, useStyles2 } from '@grafana/ui'; import { Button, Dropdown, Icon, Menu, MenuItem, Stack, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter'; import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
@@ -34,7 +35,16 @@ export const ReceiversSection = ({
const showMore = showExport; const showMore = showExport;
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const newMenu = <Menu>{showExport && <MenuItem onClick={toggleShowExportDrawer} label="Export all" />}</Menu>; const newMenu = (
<Menu>
{showExport && (
<MenuItem
onClick={toggleShowExportDrawer}
label={t('alerting.receivers-section.new-menu.label-export-all', 'Export all')}
/>
)}
</Menu>
);
return ( return (
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
@@ -3,6 +3,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack, useStyles2 } from '@grafana/ui'; import { Stack, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { PopupCard } from '../HoverCard'; import { PopupCard } from '../HoverCard';
@@ -83,9 +84,15 @@ export function TemplateDataTable({ dataItems, caption, typeRenderer }: Template
{caption && <caption>{caption}</caption>} {caption && <caption>{caption}</caption>}
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>
<th>Type</th> <Trans i18nKey="alerting.template-data-table.name">Name</Trans>
<th>Notes</th> </th>
<th>
<Trans i18nKey="alerting.template-data-table.type">Type</Trans>
</th>
<th>
<Trans i18nKey="alerting.template-data-table.notes">Notes</Trans>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -111,13 +118,23 @@ function KeyValueTemplateDataTable() {
<code>{KeyValueCodeSnippet}</code> <code>{KeyValueCodeSnippet}</code>
</pre> </pre>
<table className={tableStyles.table}> <table className={tableStyles.table}>
<caption>Key-value methods</caption> <caption>
<Trans i18nKey="alerting.key-value-template-data-table.keyvalue-methods">Key-value methods</Trans>
</caption>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>
<th>Arguments</th> <Trans i18nKey="alerting.key-value-template-data-table.name">Name</Trans>
<th>Returns</th> </th>
<th>Notes</th> <th>
<Trans i18nKey="alerting.key-value-template-data-table.arguments">Arguments</Trans>
</th>
<th>
<Trans i18nKey="alerting.key-value-template-data-table.returns">Returns</Trans>
</th>
<th>
<Trans i18nKey="alerting.key-value-template-data-table.notes">Notes</Trans>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -167,10 +167,18 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
return ( return (
<> <>
<FormProvider {...formApi}> <FormProvider {...formApi}>
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form"> <form
onSubmit={handleSubmit(submit)}
ref={formRef}
className={styles.form}
aria-label={t('alerting.template-form.aria-label-template-form', 'Template form')}
>
{/* error message */} {/* error message */}
{error && ( {error && (
<Alert severity="error" title="Error saving template"> <Alert
severity="error"
title={t('alerting.template-form.title-error-saving-template', 'Error saving template')}
>
{error.message || (isFetchError(error) && error.data?.message) || String(error)} {error.message || (isFetchError(error) && error.data?.message) || String(error)}
</Alert> </Alert>
)} )}
@@ -187,7 +195,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
{/* name and save buttons */} {/* name and save buttons */}
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<InlineField <InlineField
label="Template group name" label={t('alerting.template-form.label-template-group-name', 'Template group name')}
error={errors?.title?.message} error={errors?.title?.message}
invalid={!!errors.title?.message} invalid={!!errors.title?.message}
required required
@@ -197,7 +205,10 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
required: { value: true, message: 'Required.' }, required: { value: true, message: 'Required.' },
validate: { titleIsUnique }, validate: { titleIsUnique },
})} })}
placeholder="Give your template group a name" placeholder={t(
'alerting.template-form.new-template-name-placeholder-give-your-template-group-a-name',
'Give your template group a name'
)}
width={42} width={42}
autoFocus={true} autoFocus={true}
id="new-template-name" id="new-template-name"
@@ -231,7 +242,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
<div className={cx(styles.flexColumn, styles.containerWithBorderAndRadius, styles.minEditorSize)}> <div className={cx(styles.flexColumn, styles.containerWithBorderAndRadius, styles.minEditorSize)}>
<div> <div>
<EditorColumnHeader <EditorColumnHeader
label="Template group" label={t('alerting.template-form.label-template-group', 'Template group')}
actions={ actions={
<> <>
{/* examples dropdown – only available for Grafana Alertmanager */} {/* examples dropdown – only available for Grafana Alertmanager */}
@@ -334,7 +345,11 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
</form> </form>
</FormProvider> </FormProvider>
{cheatsheetOpened && ( {cheatsheetOpened && (
<Drawer title="Templating cheat sheet" onClose={toggleCheatsheetOpened} size="lg"> <Drawer
title={t('alerting.template-form.title-templating-cheat-sheet', 'Templating cheat sheet')}
onClose={toggleCheatsheetOpened}
size="lg"
>
<TemplatingCheatSheet /> <TemplatingCheatSheet />
</Drawer> </Drawer>
)} )}
@@ -6,6 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Box, Button, useStyles2 } from '@grafana/ui'; import { Alert, Box, Button, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi'; import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi';
import { stringifyErrorLike } from '../../utils/misc'; import { stringifyErrorLike } from '../../utils/misc';
@@ -44,17 +45,17 @@ export function TemplatePreview({
return ( return (
<div className={cx(styles.container, className)}> <div className={cx(styles.container, className)}>
<EditorColumnHeader <EditorColumnHeader
label="Preview" label={t('alerting.template-preview.label-preview', 'Preview')}
actions={ actions={
<Button <Button
disabled={isLoading} disabled={isLoading}
icon="sync" icon="sync"
aria-label="Refresh preview" aria-label={t('alerting.template-preview.aria-label-refresh-preview', 'Refresh preview')}
onClick={onPreview} onClick={onPreview}
size="sm" size="sm"
variant="secondary" variant="secondary"
> >
Refresh <Trans i18nKey="alerting.template-preview.refresh">Refresh</Trans>
</Button> </Button>
} }
/> />
@@ -152,7 +153,7 @@ export function getPreviewResults(
return ( return (
<> <>
{errorToRender && ( {errorToRender && (
<Alert severity="error" title="Error"> <Alert severity="error" title={t('alerting.get-preview-results.title-error', 'Error')}>
{errorToRender} {errorToRender}
</Alert> </Alert>
)} )}
@@ -62,7 +62,9 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
<thead> <thead>
<tr> <tr>
<th /> <th />
<th>Template group</th> <th>
<Trans i18nKey="alerting.templates-table.template-group">Template group</Trans>
</th>
<Authorize <Authorize
actions={[ actions={[
AlertmanagerAction.CreateNotificationTemplate, AlertmanagerAction.CreateNotificationTemplate,
@@ -70,14 +72,18 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
AlertmanagerAction.DeleteNotificationTemplate, AlertmanagerAction.DeleteNotificationTemplate,
]} ]}
> >
<th>Actions</th> <th>
<Trans i18nKey="alerting.templates-table.actions">Actions</Trans>
</th>
</Authorize> </Authorize>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{!templates.length && ( {!templates.length && (
<tr className={tableStyles.evenRow}> <tr className={tableStyles.evenRow}>
<td colSpan={3}>No templates defined.</td> <td colSpan={3}>
<Trans i18nKey="alerting.templates-table.no-templates-defined">No templates defined.</Trans>
</td>
</tr> </tr>
)} )}
{templates.map((notificationTemplate, idx) => ( {templates.map((notificationTemplate, idx) => (
@@ -95,7 +101,7 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
{!!templateToDelete && ( {!!templateToDelete && (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}
title="Delete template group" title={t('alerting.templates-table.title-delete-template-group', 'Delete template group')}
body={`Are you sure you want to delete template group "${templateToDelete.title}"?`} body={`Are you sure you want to delete template group "${templateToDelete.title}"?`}
confirmText="Yes, delete" confirmText="Yes, delete"
onConfirm={onDeleteTemplate} onConfirm={onDeleteTemplate}
@@ -153,7 +159,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
{isProvisioned && ( {isProvisioned && (
<ActionIcon <ActionIcon
to={makeAMLink(`/alerting/notifications/templates/${encodeURIComponent(uid)}/edit`, alertManagerName)} to={makeAMLink(`/alerting/notifications/templates/${encodeURIComponent(uid)}/edit`, alertManagerName)}
tooltip="view template" tooltip={t('alerting.template-row.tooltip-view-template', 'view template')}
icon="file-alt" icon="file-alt"
/> />
)} )}
@@ -161,7 +167,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
<Authorize actions={[AlertmanagerAction.UpdateNotificationTemplate]}> <Authorize actions={[AlertmanagerAction.UpdateNotificationTemplate]}>
<ActionIcon <ActionIcon
to={makeAMLink(`/alerting/notifications/templates/${encodeURIComponent(uid)}/edit`, alertManagerName)} to={makeAMLink(`/alerting/notifications/templates/${encodeURIComponent(uid)}/edit`, alertManagerName)}
tooltip="Edit template group" tooltip={t('alerting.template-row.tooltip-edit-template-group', 'Edit template group')}
icon="pen" icon="pen"
/> />
</Authorize> </Authorize>
@@ -172,7 +178,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
`/alerting/notifications/templates/${encodeURIComponent(uid)}/duplicate`, `/alerting/notifications/templates/${encodeURIComponent(uid)}/duplicate`,
alertManagerName alertManagerName
)} )}
tooltip="Copy template group" tooltip={t('alerting.template-row.tooltip-copy-template-group', 'Copy template group')}
icon="copy" icon="copy"
/> />
</Authorize> </Authorize>
@@ -180,7 +186,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
<Authorize actions={[AlertmanagerAction.DeleteNotificationTemplate]}> <Authorize actions={[AlertmanagerAction.DeleteNotificationTemplate]}>
<ActionIcon <ActionIcon
onClick={() => onDeleteClick(notificationTemplate)} onClick={() => onDeleteClick(notificationTemplate)}
tooltip="Delete template group" tooltip={t('alerting.template-row.tooltip-delete-template-group', 'Delete template group')}
icon="trash-alt" icon="trash-alt"
/> />
</Authorize> </Authorize>
@@ -145,7 +145,11 @@ export function ChannelSubForm<R extends ChannelValues>({
<div className={styles.wrapper} data-testid="item-container"> <div className={styles.wrapper} data-testid="item-container">
<div className={styles.topRow}> <div className={styles.topRow}>
<div> <div>
<Field label="Integration" htmlFor={contactPointTypeInputId} data-testid={`${pathPrefix}type`}> <Field
label={t('alerting.channel-sub-form.label-integration', 'Integration')}
htmlFor={contactPointTypeInputId}
data-testid={`${pathPrefix}type`}
>
<Controller <Controller
name={fieldName('type')} name={fieldName('type')}
defaultValue={defaultValues.type} defaultValue={defaultValues.type}
@@ -174,13 +178,13 @@ export function ChannelSubForm<R extends ChannelValues>({
onClick={() => handleTest()} onClick={() => handleTest()}
icon={testingReceiver ? 'spinner' : 'message'} icon={testingReceiver ? 'spinner' : 'message'}
> >
Test <Trans i18nKey="alerting.channel-sub-form.test">Test</Trans>
</Button> </Button>
)} )}
{isEditable && ( {isEditable && (
<> <>
<Button size="xs" variant="secondary" type="button" onClick={() => onDuplicate()} icon="copy"> <Button size="xs" variant="secondary" type="button" onClick={() => onDuplicate()} icon="copy">
Duplicate <Trans i18nKey="alerting.channel-sub-form.duplicate">Duplicate</Trans>
</Button> </Button>
{onDelete && ( {onDelete && (
<Button <Button
@@ -191,7 +195,7 @@ export function ChannelSubForm<R extends ChannelValues>({
onClick={() => onDelete()} onClick={() => onDelete()}
icon="trash-alt" icon="trash-alt"
> >
Delete <Trans i18nKey="alerting.channel-sub-form.delete">Delete</Trans>
</Button> </Button>
)} )}
</> </>
@@ -244,7 +248,9 @@ export function ChannelSubForm<R extends ChannelValues>({
/> />
</CollapsibleSection> </CollapsibleSection>
)} )}
<CollapsibleSection label="Notification settings"> <CollapsibleSection
label={t('alerting.channel-sub-form.label-notification-settings', 'Notification settings')}
>
<CommonSettingsComponent pathPrefix={pathPrefix} readOnly={!isEditable} /> <CommonSettingsComponent pathPrefix={pathPrefix} readOnly={!isEditable} />
</CollapsibleSection> </CollapsibleSection>
</div> </div>
@@ -1,6 +1,7 @@
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Checkbox, Field } from '@grafana/ui'; import { Checkbox, Field } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { CommonSettingsComponentProps } from '../../../types/receiver-form'; import { CommonSettingsComponentProps } from '../../../types/receiver-form';
@@ -15,9 +16,12 @@ export const CloudCommonChannelSettings = ({
<Field disabled={readOnly}> <Field disabled={readOnly}>
<Checkbox <Checkbox
{...register(`${pathPrefix}sendResolved`)} {...register(`${pathPrefix}sendResolved`)}
label="Send resolved" label={t('alerting.cloud-common-channel-settings.label-send-resolved', 'Send resolved')}
disabled={readOnly} disabled={readOnly}
description="Whether or not to notify about resolved alerts." description={t(
'alerting.cloud-common-channel-settings.description-whether-notify-about-resolved-alerts',
'Whether or not to notify about resolved alerts.'
)}
/> />
</Field> </Field>
</div> </div>
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Alert } from '@grafana/ui'; import { Alert } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
import { import {
useCreateContactPoint, useCreateContactPoint,
@@ -74,7 +75,7 @@ export const CloudReceiverForm = ({ contactPoint, alertManagerSourceName, readOn
return ( return (
<> <>
{!isVanillaAM && ( {!isVanillaAM && (
<Alert title="Info" severity="info"> <Alert title={t('alerting.cloud-receiver-form.title-info', 'Info')} severity="info">
Note that empty string values will be replaced with global defaults where appropriate. Note that empty string values will be replaced with global defaults where appropriate.
</Alert> </Alert>
)} )}
@@ -6,6 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Card, Modal, RadioButtonGroup, Stack, useStyles2 } from '@grafana/ui'; import { Button, Card, Modal, RadioButtonGroup, Stack, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { KeyValueField } from '../../../api/templateApi'; import { KeyValueField } from '../../../api/templateApi';
@@ -115,7 +116,7 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
variant="secondary" variant="secondary"
disabled={!labelsOrAnnotationsAdded()} disabled={!labelsOrAnnotationsAdded()}
> >
Add alert data <Trans i18nKey="alerting.generate-alert-data-modal.add-alert-data">Add alert data</Trans>
</Button> </Button>
</div> </div>
</Stack> </Stack>
@@ -123,7 +124,12 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
<div className={styles.onSubmitWrapper} /> <div className={styles.onSubmitWrapper} />
{alerts.length > 0 && ( {alerts.length > 0 && (
<Stack direction="column" gap={1}> <Stack direction="column" gap={1}>
<h5> Review alert data to add to the payload:</h5> <h5>
<Trans i18nKey="alerting.generate-alert-data-modal.review-alert-payload">
{' '}
Review alert data to add to the payload:
</Trans>
</h5>
<pre className={styles.result} data-testid="payloadJSON"> <pre className={styles.result} data-testid="payloadJSON">
{JSON.stringify(alerts, null, 2)} {JSON.stringify(alerts, null, 2)}
</pre> </pre>
@@ -132,7 +138,9 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
<div className={styles.onSubmitWrapper}> <div className={styles.onSubmitWrapper}>
<Modal.ButtonRow> <Modal.ButtonRow>
<Button onClick={onSubmit} disabled={alerts.length === 0} className={styles.onSubmitButton}> <Button onClick={onSubmit} disabled={alerts.length === 0} className={styles.onSubmitButton}>
Add alert data to payload <Trans i18nKey="alerting.generate-alert-data-modal.add-alert-data-to-payload">
Add alert data to payload
</Trans>
</Button> </Button>
</Modal.ButtonRow> </Modal.ButtonRow>
</div> </div>
@@ -1,6 +1,7 @@
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Checkbox, Field } from '@grafana/ui'; import { Checkbox, Field } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { CommonSettingsComponentProps } from '../../../types/receiver-form'; import { CommonSettingsComponentProps } from '../../../types/receiver-form';
@@ -15,7 +16,10 @@ export const GrafanaCommonChannelSettings = ({
<Field> <Field>
<Checkbox <Checkbox
{...register(`${pathPrefix}disableResolveMessage`)} {...register(`${pathPrefix}disableResolveMessage`)}
label="Disable resolved message" label={t(
'alerting.grafana-common-channel-settings.label-disable-resolved-message',
'Disable resolved message'
)}
description="Disable the resolve message [OK] that is sent when alerting state returns to false" description="Disable the resolve message [OK] that is sent when alerting state returns to false"
disabled={readOnly} disabled={readOnly}
/> />
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { import {
useCreateContactPoint, useCreateContactPoint,
useUpdateContactPoint, useUpdateContactPoint,
@@ -142,7 +143,9 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
const isTestable = !readOnly; const isTestable = !readOnly;
if (isLoadingNotifiers || isLoadingOnCallIntegration) { if (isLoadingNotifiers || isLoadingOnCallIntegration) {
return <LoadingPlaceholder text="Loading notifiers..." />; return (
<LoadingPlaceholder text={t('alerting.grafana-receiver-form.text-loading-notifiers', 'Loading notifiers...')} />
);
} }
const notifiers: Notifier[] = grafanaNotifiers.map((n) => { const notifiers: Notifier[] = grafanaNotifiers.map((n) => {
@@ -158,7 +161,13 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
return ( return (
<> <>
{hasOnCallError && ( {hasOnCallError && (
<Alert severity="error" title="Loading OnCall integration failed"> <Alert
severity="error"
title={t(
'alerting.grafana-receiver-form.title-loading-on-call-integration-failed',
'Loading OnCall integration failed'
)}
>
Grafana OnCall plugin has been enabled in your Grafana instances but it is not reachable. Please check the Grafana OnCall plugin has been enabled in your Grafana instances but it is not reachable. Please check the
plugin configuration plugin configuration
</Alert> </Alert>
@@ -7,7 +7,7 @@ import { isFetchError } from '@grafana/runtime';
import { Alert, Button, Field, Input, LinkButton, Stack, useStyles2 } from '@grafana/ui'; import { Alert, Button, Field, Input, LinkButton, Stack, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { useValidateContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useValidateContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { ManagePermissions } from 'app/features/alerting/unified/components/permissions/ManagePermissions'; import { ManagePermissions } from 'app/features/alerting/unified/components/permissions/ManagePermissions';
@@ -128,7 +128,7 @@ export function ReceiverForm<R extends ChannelValues>({
return ( return (
<FormProvider {...formAPI}> <FormProvider {...formAPI}>
{showDefaultRouteWarning && ( {showDefaultRouteWarning && (
<Alert severity="warning" title="Attention"> <Alert severity="warning" title={t('alerting.receiver-form.title-attention', 'Attention')}>
Because there is no default policy configured yet, this contact point will automatically be set as default. Because there is no default policy configured yet, this contact point will automatically be set as default.
</Alert> </Alert>
)} )}
@@ -143,11 +143,19 @@ export function ReceiverForm<R extends ChannelValues>({
resource="receivers" resource="receivers"
resourceId={contactPointId} resourceId={contactPointId}
resourceName={initialValues?.name} resourceName={initialValues?.name}
title="Manage contact point permissions" title={t(
'alerting.receiver-form.title-manage-contact-point-permissions',
'Manage contact point permissions'
)}
/> />
)} )}
</Stack> </Stack>
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message} required> <Field
label={t('alerting.receiver-form.label-name', 'Name')}
invalid={!!errors.name}
error={errors.name && errors.name.message}
required
>
<Input <Input
readOnly={!isEditable} readOnly={!isEditable}
id="name" id="name"
@@ -159,7 +167,7 @@ export function ReceiverForm<R extends ChannelValues>({
}, },
})} })}
width={39} width={39}
placeholder="Name" placeholder={t('alerting.receiver-form.name-placeholder-name', 'Name')}
/> />
</Field> </Field>
{fields.map((field, index) => { {fields.map((field, index) => {
@@ -204,7 +212,7 @@ export function ReceiverForm<R extends ChannelValues>({
variant="secondary" variant="secondary"
onClick={() => append({ ...defaultItem, __id: String(Math.random()) })} onClick={() => append({ ...defaultItem, __id: String(Math.random()) })}
> >
Add contact point integration <Trans i18nKey="alerting.receiver-form.add-contact-point-integration">Add contact point integration</Trans>
</Button> </Button>
)} )}
<div className={styles.buttons}> <div className={styles.buttons}>
@@ -212,10 +220,14 @@ export function ReceiverForm<R extends ChannelValues>({
<> <>
{isSubmitting && ( {isSubmitting && (
<Button disabled={true} icon="spinner" variant="primary"> <Button disabled={true} icon="spinner" variant="primary">
Saving... <Trans i18nKey="alerting.receiver-form.saving">Saving...</Trans>
</Button>
)}
{!isSubmitting && (
<Button type="submit">
<Trans i18nKey="alerting.receiver-form.save-contact-point">Save contact point</Trans>
</Button> </Button>
)} )}
{!isSubmitting && <Button type="submit">Save contact point</Button>}
</> </>
)} )}
<LinkButton <LinkButton
@@ -4,6 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types'; import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types';
import { Annotations, Labels } from 'app/types/unified-alerting-dto'; import { Annotations, Labels } from 'app/types/unified-alerting-dto';
@@ -67,7 +68,9 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
return ( return (
<Modal onDismiss={onDismiss} isOpen={isOpen} title={'Test contact point'}> <Modal onDismiss={onDismiss} isOpen={isOpen} title={'Test contact point'}>
<div className={styles.section}> <div className={styles.section}>
<Label>Notification message</Label> <Label>
<Trans i18nKey="alerting.test-contact-point-modal.notification-message">Notification message</Trans>
</Label>
<RadioButtonGroup <RadioButtonGroup
options={notificationOptions} options={notificationOptions}
value={notificationType} value={notificationType}
@@ -99,7 +102,9 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
)} )}
<Modal.ButtonRow> <Modal.ButtonRow>
<Button type="submit">Send test notification</Button> <Button type="submit">
<Trans i18nKey="alerting.test-contact-point-modal.send-test-notification">Send test notification</Trans>
</Button>
</Modal.ButtonRow> </Modal.ButtonRow>
</form> </form>
</FormProvider> </FormProvider>
@@ -37,8 +37,12 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
<table className={styles.table}> <table className={styles.table}>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>
<th>Value</th> <Trans i18nKey="alerting.key-value-map-input.name">Name</Trans>
</th>
<th>
<Trans i18nKey="alerting.key-value-map-input.value">Value</Trans>
</th>
{!readOnly && <th />} {!readOnly && <th />}
</tr> </tr>
</thead> </thead>
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Input, useStyles2 } from '@grafana/ui'; import { Button, Input, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { ActionIcon } from '../../../rules/ActionIcon'; import { ActionIcon } from '../../../rules/ActionIcon';
@@ -40,7 +41,7 @@ export const StringArrayInput = ({ value, onChange, readOnly = false }: Props) =
<ActionIcon <ActionIcon
className={styles.deleteIcon} className={styles.deleteIcon}
icon="trash-alt" icon="trash-alt"
tooltip="delete" tooltip={t('alerting.string-array-input.tooltip-delete', 'delete')}
onClick={() => deleteItem(index)} onClick={() => deleteItem(index)}
/> />
)} )}
@@ -55,7 +56,7 @@ export const StringArrayInput = ({ value, onChange, readOnly = false }: Props) =
size="sm" size="sm"
onClick={() => onChange([...(value ?? []), ''])} onClick={() => onChange([...(value ?? []), ''])}
> >
Add <Trans i18nKey="alerting.string-array-input.add">Add</Trans>
</Button> </Button>
)} )}
</div> </div>
@@ -1,6 +1,7 @@
import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { DeepMap, FieldError, useFormContext } from 'react-hook-form';
import { Button, useStyles2 } from '@grafana/ui'; import { Button, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useControlledFieldArray } from 'app/features/alerting/unified/hooks/useControlledFieldArray'; import { useControlledFieldArray } from 'app/features/alerting/unified/hooks/useControlledFieldArray';
import { NotificationChannelOption } from 'app/types'; import { NotificationChannelOption } from 'app/types';
@@ -38,7 +39,7 @@ export const SubformArrayField = ({ option, pathPrefix, errors, defaultValues, r
<ActionIcon <ActionIcon
data-testid={`${path}.${itemIndex}.delete-button`} data-testid={`${path}.${itemIndex}.delete-button`}
icon="trash-alt" icon="trash-alt"
tooltip="delete" tooltip={t('alerting.subform-array-field.tooltip-delete', 'delete')}
onClick={() => remove(itemIndex)} onClick={() => remove(itemIndex)}
className={styles.deleteIcon} className={styles.deleteIcon}
/> />
@@ -66,7 +67,7 @@ export const SubformArrayField = ({ option, pathPrefix, errors, defaultValues, r
size="sm" size="sm"
onClick={() => append({ __id: String(Math.random()) })} onClick={() => append({ __id: String(Math.random()) })}
> >
Add <Trans i18nKey="alerting.subform-array-field.add">Add</Trans>
</Button> </Button>
)} )}
</CollapsibleSection> </CollapsibleSection>
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { DeepMap, FieldError, useFormContext } from 'react-hook-form';
import { Button, useStyles2 } from '@grafana/ui'; import { Button, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types'; import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
import { ActionIcon } from '../../../rules/ActionIcon'; import { ActionIcon } from '../../../rules/ActionIcon';
@@ -46,7 +47,7 @@ export const SubformField = ({
<ActionIcon <ActionIcon
data-testid={`${name}.delete-button`} data-testid={`${name}.delete-button`}
icon="trash-alt" icon="trash-alt"
tooltip="delete" tooltip={t('alerting.subform-field.tooltip-delete', 'delete')}
onClick={() => setShow(false)} onClick={() => setShow(false)}
className={styles.deleteIcon} className={styles.deleteIcon}
/> />
@@ -78,7 +79,7 @@ export const SubformField = ({
onClick={() => setShow(true)} onClick={() => setShow(true)}
data-testid={`${name}.add-button`} data-testid={`${name}.add-button`}
> >
Add <Trans i18nKey="alerting.subform-field.add">Add</Trans>
</Button> </Button>
)} )}
</div> </div>
@@ -4,6 +4,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Box, useStyles2 } from '@grafana/ui'; import { Box, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
@@ -40,7 +41,9 @@ export function TemplateContentAndPreview({
return ( return (
<div className={cx(className, styles.mainContainer)}> <div className={cx(className, styles.mainContainer)}>
<div className={styles.container}> <div className={styles.container}>
<EditorColumnHeader label="Template content" /> <EditorColumnHeader
label={t('alerting.template-content-and-preview.label-template-content', 'Template content')}
/>
<Box flex={1}> <Box flex={1}>
<div className={styles.viewerContainer({ height: 400 })}> <div className={styles.viewerContainer({ height: 400 })}>
<AutoSizer> <AutoSizer>
@@ -60,7 +63,13 @@ export function TemplateContentAndPreview({
{isGrafanaAlertManager && ( {isGrafanaAlertManager && (
<div className={styles.container}> <div className={styles.container}>
<EditorColumnHeader id={templatePreviewId} label="Preview with the default payload" /> <EditorColumnHeader
id={templatePreviewId}
label={t(
'alerting.template-content-and-preview.label-preview-with-the-default-payload',
'Preview with the default payload'
)}
/>
<Box flex={1}> <Box flex={1}>
<div <div
role="presentation" role="presentation"
@@ -16,7 +16,7 @@ import {
TextArea, TextArea,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { import {
trackEditInputWithTemplate, trackEditInputWithTemplate,
trackUseCustomInputInTemplate, trackUseCustomInputInTemplate,
@@ -173,11 +173,19 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
}, [options, valueInForm]); }, [options, valueInForm]);
if (error) { if (error) {
return <div>Error loading templates</div>; return (
<div>
<Trans i18nKey="alerting.template-selector.error-loading-templates">Error loading templates</Trans>
</div>
);
} }
if (isLoading || !data || !defaultTemplates) { if (isLoading || !data || !defaultTemplates) {
return <div>Loading...</div>; return (
<div>
<Trans i18nKey="alerting.template-selector.loading">Loading...</Trans>
</div>
);
} }
return ( return (
@@ -195,8 +203,14 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
<Stack direction="row" gap={1} alignItems="center"> <Stack direction="row" gap={1} alignItems="center">
<Select<Template> <Select<Template>
data-testid="existing-templates-selector" data-testid="existing-templates-selector"
placeholder="Choose notification template" placeholder={t(
aria-label="Choose notification template" 'alerting.template-selector.existing-templates-selector-placeholder-choose-notification-template',
'Choose notification template'
)}
aria-label={t(
'alerting.template-selector.existing-templates-selector-aria-label-choose-notification-template',
'Choose notification template'
)}
onChange={(value: SelectableValue<Template>, _) => { onChange={(value: SelectableValue<Template>, _) => {
setTemplate(value); setTemplate(value);
}} }}
@@ -272,7 +286,7 @@ function OptionCustomfield({
</Label> </Label>
<TextArea <TextArea
id={id} id={id}
label="Custom template" label={t('alerting.option-customfield.label-custom-template', 'Custom template')}
placeholder={option.placeholder} placeholder={option.placeholder}
onChange={(e) => onCustomTemplateChange(e.currentTarget.value)} onChange={(e) => onCustomTemplateChange(e.currentTarget.value)}
defaultValue={initialValue} defaultValue={initialValue}
@@ -53,7 +53,11 @@ export const AlertRuleNameAndMetric = () => {
} }
> >
<Stack direction="column"> <Stack direction="column">
<Field label="Name" error={errors?.name?.message} invalid={!!errors.name?.message}> <Field
label={t('alerting.alert-rule-name-and-metric.label-name', 'Name')}
error={errors?.name?.message}
invalid={!!errors.name?.message}
>
<Input <Input
data-testid={selectors.components.AlertRules.ruleNameField} data-testid={selectors.components.AlertRules.ruleNameField}
id="name" id="name"
@@ -64,12 +68,16 @@ export const AlertRuleNameAndMetric = () => {
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording) ? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
: undefined, : undefined,
})} })}
aria-label="name" aria-label={t('alerting.alert-rule-name-and-metric.aria-label-name', 'name')}
placeholder={`Give your ${namePlaceholder} a name`} placeholder={`Give your ${namePlaceholder} a name`}
/> />
</Field> </Field>
{isGrafanaRecordingRule && ( {isGrafanaRecordingRule && (
<Field label="Metric" error={errors?.metric?.message} invalid={!!errors.metric?.message}> <Field
label={t('alerting.alert-rule-name-and-metric.label-metric', 'Metric')}
error={errors?.metric?.message}
invalid={!!errors.metric?.message}
>
<Input <Input
id="metric" id="metric"
width={38} width={38}
@@ -77,7 +85,7 @@ export const AlertRuleNameAndMetric = () => {
required: { value: true, message: 'Must enter a metric name' }, required: { value: true, message: 'Must enter a metric name' },
pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording), pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording),
})} })}
aria-label="metric" aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')}
placeholder={`Give the name of the new recorded metric`} placeholder={`Give the name of the new recorded metric`}
/> />
</Field> </Field>
@@ -122,7 +122,7 @@ const AnnotationsStep = () => {
)} )}
</> </>
} }
title="Annotations" title={t('alerting.annotations-step.get-annotations-section-description.title-annotations', 'Annotations')}
/> />
</Stack> </Stack>
); );
@@ -190,7 +190,7 @@ const AnnotationsStep = () => {
<Button <Button
type="button" type="button"
className={styles.deleteAnnotationButton} className={styles.deleteAnnotationButton}
aria-label="delete annotation" aria-label={t('alerting.annotations-step.aria-label-delete-annotation', 'delete annotation')}
icon="trash-alt" icon="trash-alt"
variant="secondary" variant="secondary"
onClick={() => remove(index)} onClick={() => remove(index)}
@@ -212,11 +212,11 @@ const AnnotationsStep = () => {
append({ key: '', value: '' }); append({ key: '', value: '' });
}} }}
> >
Add custom annotation <Trans i18nKey="alerting.annotations-step.add-custom-annotation">Add custom annotation</Trans>
</Button> </Button>
{!selectedDashboard && ( {!selectedDashboard && (
<Button type="button" variant="secondary" icon="dashboard" onClick={() => setShowPanelSelector(true)}> <Button type="button" variant="secondary" icon="dashboard" onClick={() => setShowPanelSelector(true)}>
Link dashboard and panel <Trans i18nKey="alerting.annotations-step.link-dashboard-and-panel">Link dashboard and panel</Trans>
</Button> </Button>
)} )}
</div> </div>
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import { Icon, TagList, Tooltip, useStyles2 } from '@grafana/ui'; import { Icon, TagList, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { labelsToTags } from '../../utils/labels'; import { labelsToTags } from '../../utils/labels';
import { AlertStateTag } from '../rules/AlertStateTag'; import { AlertStateTag } from '../rules/AlertStateTag';
@@ -19,14 +20,22 @@ export function CloudAlertPreview({ preview }: CloudAlertPreviewProps) {
return ( return (
<table className={styles.table}> <table className={styles.table}>
<caption> <caption>
<div>Alerts preview</div> <div>
<Trans i18nKey="alerting.cloud-alert-preview.alerts-preview">Alerts preview</Trans>
</div>
<span>Preview based on the result of running the query for this moment.</span> <span>Preview based on the result of running the query for this moment.</span>
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th>State</th> <th>
<th>Labels</th> <Trans i18nKey="alerting.cloud-alert-preview.state">State</Trans>
<th>Info</th> </th>
<th>
<Trans i18nKey="alerting.cloud-alert-preview.labels">Labels</Trans>
</th>
<th>
<Trans i18nKey="alerting.cloud-alert-preview.info">Info</Trans>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -3,6 +3,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Field, Input, Select, useStyles2 } from '@grafana/ui'; import { Field, Input, Select, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { timeOptions } from '../../utils/time'; import { timeOptions } from '../../utils/time';
@@ -24,9 +25,12 @@ export const CloudEvaluationBehavior = () => {
const dataSourceName = watch('dataSourceName'); const dataSourceName = watch('dataSourceName');
return ( return (
<RuleEditorSection stepNo={3} title="Set evaluation behavior"> <RuleEditorSection
stepNo={3}
title={t('alerting.cloud-evaluation-behavior.title-set-evaluation-behavior', 'Set evaluation behavior')}
>
<Field <Field
label="Pending period" label={t('alerting.cloud-evaluation-behavior.label-pending-period', 'Pending period')}
description='Period during which the threshold condition must be met to trigger an alert. Selecting "None" triggers the alert immediately once the condition is met.' description='Period during which the threshold condition must be met to trigger an alert. Selecting "None" triggers the alert immediately once the condition is met.'
> >
<div className={styles.flexRow}> <div className={styles.flexRow}>
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Input, useStyles2 } from '@grafana/ui'; import { Input, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
interface CustomAnnotationHeaderFieldProps { interface CustomAnnotationHeaderFieldProps {
field: { onChange: () => void; onBlur: () => void; value: string; name: string }; field: { onChange: () => void; onBlur: () => void; value: string; name: string };
@@ -12,9 +13,16 @@ const CustomAnnotationHeaderField = ({ field }: CustomAnnotationHeaderFieldProps
return ( return (
<div> <div>
<span className={styles.annotationTitle}>Custom annotation name and content</span> <span className={styles.annotationTitle}>
<Trans i18nKey="alerting.custom-annotation-header-field.custom-annotation-name-and-content">
Custom annotation name and content
</Trans>
</span>
<Input <Input
placeholder="Enter custom annotation name..." placeholder={t(
'alerting.custom-annotation-header-field.placeholder-enter-custom-annotation-name',
'Enter custom annotation name...'
)}
width={18} width={18}
{...field} {...field}
className={styles.customAnnotationInput} className={styles.customAnnotationInput}
@@ -17,7 +17,7 @@ import {
clearButtonStyles, clearButtonStyles,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
import { dashboardApi } from '../../api/dashboardApi'; import { dashboardApi } from '../../api/dashboardApi';
@@ -168,7 +168,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
return ( return (
<Modal <Modal
title="Select dashboard and panel" title={t('alerting.dashboard-picker.title-select-dashboard-and-panel', 'Select dashboard and panel')}
closeOnEscape closeOnEscape
isOpen={isOpen} isOpen={isOpen}
onDismiss={onDismiss} onDismiss={onDismiss}
@@ -177,7 +177,13 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
> >
{/* This alert shows if the selected dashboard is not found in the first page of dashboards */} {/* This alert shows if the selected dashboard is not found in the first page of dashboards */}
{!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && ( {!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && (
<Alert title="Current selection" severity="info" topSpacing={0} bottomSpacing={1} className={styles.modalAlert}> <Alert
title={t('alerting.dashboard-picker.title-current-selection', 'Current selection')}
severity="info"
topSpacing={0}
bottomSpacing={1}
className={styles.modalAlert}
>
<div> <div>
Dashboard: {dashboardModel.title} ({dashboardModel.uid}) in folder{' '} Dashboard: {dashboardModel.title} ({dashboardModel.uid}) in folder{' '}
{dashboardModel.meta?.folderTitle ?? 'Dashboards'} {dashboardModel.meta?.folderTitle ?? 'Dashboards'}
@@ -193,15 +199,23 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
<FilterInput <FilterInput
value={dashboardFilter} value={dashboardFilter}
onChange={setDashboardFilter} onChange={setDashboardFilter}
title="Search dashboard" title={t('alerting.dashboard-picker.title-search-dashboard', 'Search dashboard')}
placeholder="Search dashboard" placeholder={t('alerting.dashboard-picker.placeholder-search-dashboard', 'Search dashboard')}
autoFocus autoFocus
/> />
<FilterInput value={panelFilter} onChange={setPanelFilter} title="Search panel" placeholder="Search panel" /> <FilterInput
value={panelFilter}
onChange={setPanelFilter}
title={t('alerting.dashboard-picker.title-search-panel', 'Search panel')}
placeholder={t('alerting.dashboard-picker.placeholder-search-panel', 'Search panel')}
/>
<div className={styles.column}> <div className={styles.column}>
{isDashSearchFetching && ( {isDashSearchFetching && (
<LoadingPlaceholder text="Loading dashboards..." className={styles.loadingPlaceholder} /> <LoadingPlaceholder
text={t('alerting.dashboard-picker.text-loading-dashboards', 'Loading dashboards...')}
className={styles.loadingPlaceholder}
/>
)} )}
{!isDashSearchFetching && ( {!isDashSearchFetching && (
@@ -224,11 +238,18 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
<div className={styles.column}> <div className={styles.column}>
{!selectedDashboardUid && !isDashboardFetching && ( {!selectedDashboardUid && !isDashboardFetching && (
<div className={styles.selectDashboardPlaceholder}> <div className={styles.selectDashboardPlaceholder}>
<div>Select a dashboard to get a list of available panels</div> <div>
<Trans i18nKey="alerting.dashboard-picker.select-dashboard-available-panels">
Select a dashboard to get a list of available panels
</Trans>
</div>
</div> </div>
)} )}
{isDashboardFetching && ( {isDashboardFetching && (
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} /> <LoadingPlaceholder
text={t('alerting.dashboard-picker.text-loading-dashboard', 'Loading dashboard...')}
className={styles.loadingPlaceholder}
/>
)} )}
{selectedDashboardUid && !isDashboardFetching && ( {selectedDashboardUid && !isDashboardFetching && (
@@ -256,7 +277,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
} }
}} }}
> >
Confirm <Trans i18nKey="alerting.dashboard-picker.confirm">Confirm</Trans>
</Button> </Button>
</Modal.ButtonRow> </Modal.ButtonRow>
</Modal> </Modal>
@@ -8,6 +8,7 @@ import { PromQuery } from '@grafana/prometheus';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { Alert, Button, useStyles2 } from '@grafana/ui'; import { Alert, Button, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { LokiQuery } from 'app/plugins/datasource/loki/types'; import { LokiQuery } from 'app/plugins/datasource/loki/types';
import { CloudAlertPreview } from './CloudAlertPreview'; import { CloudAlertPreview } from './CloudAlertPreview';
@@ -91,11 +92,17 @@ export const ExpressionEditor = ({
onClick={onRunQueriesClick} onClick={onRunQueriesClick}
disabled={alertPreview?.data.state === LoadingState.Loading} disabled={alertPreview?.data.state === LoadingState.Loading}
> >
Preview alerts <Trans i18nKey="alerting.expression-editor.preview-alerts">Preview alerts</Trans>
</Button> </Button>
{previewLoaded && !previewHasAlerts && ( {previewLoaded && !previewHasAlerts && (
<Alert title="Alerts preview" severity="info" className={styles.previewAlert}> <Alert
There are no firing alerts for your query. title={t('alerting.expression-editor.title-alerts-preview', 'Alerts preview')}
severity="info"
className={styles.previewAlert}
>
<Trans i18nKey="alerting.expression-editor.there-firing-alerts-query">
There are no firing alerts for your query.
</Trans>
</Alert> </Alert>
)} )}
{previewHasAlerts && <CloudAlertPreview preview={previewDataFrame} />} {previewHasAlerts && <CloudAlertPreview preview={previewDataFrame} />}
@@ -199,7 +199,7 @@ export function GrafanaEvaluationBehaviorStep({
// TODO remove "and alert condition" for recording rules // TODO remove "and alert condition" for recording rules
<RuleEditorSection <RuleEditorSection
stepNo={step} stepNo={step}
title="Set evaluation behavior" title={t('alerting.grafana-evaluation-behavior-step.title-set-evaluation-behavior', 'Set evaluation behavior')}
description={getDescription(isGrafanaRecordingRule)} description={getDescription(isGrafanaRecordingRule)}
> >
<Stack direction="column" justify-content="flex-start" align-items="flex-start"> <Stack direction="column" justify-content="flex-start" align-items="flex-start">
@@ -252,7 +252,9 @@ export function GrafanaEvaluationBehaviorStep({
</Field> </Field>
</div> </div>
<Box gap={1} display={'flex'} alignItems={'center'}> <Box gap={1} display={'flex'} alignItems={'center'}>
<Text color="secondary">or</Text> <Text color="secondary">
<Trans i18nKey="alerting.grafana-evaluation-behavior-step.or">or</Trans>
</Text>
<Button <Button
onClick={onOpenEvaluationGroupCreationModal} onClick={onOpenEvaluationGroupCreationModal}
type="button" type="button"
@@ -322,7 +324,10 @@ export function GrafanaEvaluationBehaviorStep({
<CollapseToggle <CollapseToggle
isCollapsed={!showErrorHandling} isCollapsed={!showErrorHandling}
onToggle={(collapsed) => setShowErrorHandling(!collapsed)} onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
text="Configure no data and error handling" text={t(
'alerting.grafana-evaluation-behavior-step.text-configure-no-data-and-error-handling',
'Configure no data and error handling'
)}
/> />
{showErrorHandling && ( {showErrorHandling && (
<> <>
@@ -451,7 +456,7 @@ function EvaluationGroupCreationModal({
className={styles.formInput} className={styles.formInput}
autoFocus={true} autoFocus={true}
id={evaluationGroupNameId} id={evaluationGroupNameId}
placeholder="Enter a name" placeholder={t('alerting.evaluation-group-creation-modal.placeholder-enter-a-name', 'Enter a name')}
{...register('group', { required: { value: true, message: 'Required.' } })} {...register('group', { required: { value: true, message: 'Required.' } })}
/> />
</Field> </Field>
@@ -459,7 +464,13 @@ function EvaluationGroupCreationModal({
<Field <Field
error={formState.errors.evaluateEvery?.message} error={formState.errors.evaluateEvery?.message}
label={ label={
<Label htmlFor={evaluateEveryId} description="How often all rules in the group are evaluated."> <Label
htmlFor={evaluateEveryId}
description={t(
'alerting.evaluation-group-creation-modal.description-often-rules-group-evaluated',
'How often all rules in the group are evaluated.'
)}
>
<Trans i18nKey="alerting.rule-form.evaluation.group.interval">Evaluation interval</Trans> <Trans i18nKey="alerting.rule-form.evaluation.group.interval">Evaluation interval</Trans>
</Label> </Label>
} }
@@ -555,7 +566,10 @@ function NeedHelpInfoForConfigureNoDataError() {
contentText="These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts." contentText="These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts."
externalLink={docsLink} externalLink={docsLink}
linkText={`Read more about this option`} linkText={`Read more about this option`}
title="Configure no data and error handling" title={t(
'alerting.need-help-info-for-configure-no-data-error.title-configure-no-data-and-error-handling',
'Configure no data and error handling'
)}
/> />
</Stack> </Stack>
); );
@@ -601,7 +615,7 @@ function getDescription(isGrafanaRecordingRule: boolean) {
} }
externalLink={docsLink} externalLink={docsLink}
linkText={`Read about evaluation and alert states`} linkText={`Read about evaluation and alert states`}
title="Alert rule evaluation" title={t('alerting.get-description.title-alert-rule-evaluation', 'Alert rule evaluation')}
/> />
</Stack> </Stack>
); );
@@ -57,7 +57,11 @@ export function GrafanaFolderAndLabelsStep() {
} }
return ( return (
<RuleEditorSection stepNo={3} title="Add folder and labels" description={<SectionDescription />}> <RuleEditorSection
stepNo={3}
title={t('alerting.grafana-folder-and-labels-step.title-add-folder-and-labels', 'Add folder and labels')}
description={<SectionDescription />}
>
<Stack direction="column" justify-content="flex-start" align-items="flex-start"> <Stack direction="column" justify-content="flex-start" align-items="flex-start">
<FolderSelector /> <FolderSelector />
<LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} /> <LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} />
@@ -4,6 +4,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, VirtualizedSelect, useStyles2 } from '@grafana/ui'; import { Field, VirtualizedSelect, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { RuleFormValues } from '../../types/rule-form'; import { RuleFormValues } from '../../types/rule-form';
@@ -44,7 +45,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
<div className={style.flexRow}> <div className={style.flexRow}>
<Field <Field
data-testid="namespace-picker" data-testid="namespace-picker"
label="Namespace" label={t('alerting.group-and-namespace-fields.namespace-picker-label-namespace', 'Namespace')}
// Disable translations as we don't intend to use this dropdown longterm, // Disable translations as we don't intend to use this dropdown longterm,
// so avoiding us adding translations for the sake of it // so avoiding us adding translations for the sake of it
// eslint-disable-next-line @grafana/no-untranslated-strings // eslint-disable-next-line @grafana/no-untranslated-strings
@@ -77,7 +78,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
</Field> </Field>
<Field <Field
data-testid="group-picker" data-testid="group-picker"
label="Group" label={t('alerting.group-and-namespace-fields.group-picker-label-group', 'Group')}
// Disable translations as we don't intend to use this dropdown longterm, // Disable translations as we don't intend to use this dropdown longterm,
// so avoiding us adding translations for the sake of it // so avoiding us adding translations for the sake of it
// eslint-disable-next-line @grafana/no-untranslated-strings // eslint-disable-next-line @grafana/no-untranslated-strings
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Stack, Text, Toggletip, useStyles2 } from '@grafana/ui'; import { Icon, Stack, Text, Toggletip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
interface NeedHelpInfoProps { interface NeedHelpInfoProps {
contentText: string | JSX.Element; contentText: string | JSX.Element;
@@ -39,7 +40,7 @@ export function NeedHelpInfo({ contentText, externalLink, linkText, title = 'Nee
<Stack direction="row" alignItems="center" gap={0.5}> <Stack direction="row" alignItems="center" gap={0.5}>
<Icon name="question-circle" size="sm" /> <Icon name="question-circle" size="sm" />
<Text variant="bodySmall" color="primary"> <Text variant="bodySmall" color="primary">
Need help? <Trans i18nKey="alerting.need-help-info.need-help">Need help?</Trans>
</Text> </Text>
</Stack> </Stack>
</div> </div>
@@ -5,6 +5,7 @@ import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui'; import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../../api/alertmanagerApi'; import { alertmanagerApi } from '../../api/alertmanagerApi';
@@ -91,7 +92,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
<Stack direction="row" gap={0.5} alignItems="center"> <Stack direction="row" gap={0.5} alignItems="center">
{isRecordingRuleByType(type) ? ( {isRecordingRuleByType(type) ? (
<Text variant="bodySmall" color="secondary"> <Text variant="bodySmall" color="secondary">
Add labels to help you better manage your recording rules. <Trans i18nKey="alerting.notifications-step.labels-better-manage-recording-rules">
Add labels to help you better manage your recording rules.
</Trans>
</Text> </Text>
) : ( ) : (
shouldAllowSimplifiedRouting && ( shouldAllowSimplifiedRouting && (
@@ -118,7 +121,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
)} )}
{shouldAllowSimplifiedRouting && ( {shouldAllowSimplifiedRouting && (
<div className={styles.configureNotifications}> <div className={styles.configureNotifications}>
<Text element="h5">Recipient</Text> <Text element="h5">
<Trans i18nKey="alerting.notifications-step.recipient">Recipient</Trans>
</Text>
</div> </div>
)} )}
{shouldAllowSimplifiedRouting ? ( // when simplified routing is enabled and is grafana rule {shouldAllowSimplifiedRouting ? ( // when simplified routing is enabled and is grafana rule
@@ -258,7 +263,7 @@ function NeedHelpInfoForNotificationPolicy() {
</Stack> </Stack>
</Stack> </Stack>
} }
title="Notification routing" title={t('alerting.need-help-info-for-notification-policy.title-notification-routing', 'Notification routing')}
/> />
); );
} }
@@ -279,7 +284,10 @@ function NeedHelpInfoForContactpoint() {
} }
externalLink="https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/" externalLink="https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/"
linkText="Read more about notifications" linkText="Read more about notifications"
title="Notify by selecting a contact point" title={t(
'alerting.need-help-info-for-contactpoint.title-notify-by-selecting-a-contact-point',
'Notify by selecting a contact point'
)}
/> />
); );
} }
@@ -8,6 +8,7 @@ import { takeWhile } from 'rxjs/operators';
import { GrafanaTheme2, LoadingState, dateTimeFormatISO } from '@grafana/data'; import { GrafanaTheme2, LoadingState, dateTimeFormatISO } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { Alert, Button, Stack, useStyles2 } from '@grafana/ui'; import { Alert, Button, Stack, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { previewAlertRule } from '../../api/preview'; import { previewAlertRule } from '../../api/preview';
import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus'; import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus';
@@ -37,11 +38,14 @@ export function PreviewRule(): React.ReactElement | null {
<Stack> <Stack>
{allDataSourcesAvailable && ( {allDataSourcesAvailable && (
<Button disabled={!isPreviewAvailable} type="button" variant="primary" onClick={onPreview}> <Button disabled={!isPreviewAvailable} type="button" variant="primary" onClick={onPreview}>
Preview alerts <Trans i18nKey="alerting.preview-rule.preview-alerts">Preview alerts</Trans>
</Button> </Button>
)} )}
{!allDataSourcesAvailable && ( {!allDataSourcesAvailable && (
<Alert title="Preview is not available" severity="warning"> <Alert
title={t('alerting.preview-rule.title-preview-is-not-available', 'Preview is not available')}
severity="warning"
>
Cannot display the query preview. Some of the data sources used in the queries are not available. Cannot display the query preview. Some of the data sources used in the queries are not available.
</Alert> </Alert>
)} )}
@@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { FieldConfigSource, FieldMatcherID, GrafanaTheme2, LoadingState } from '@grafana/data'; import { FieldConfigSource, FieldMatcherID, GrafanaTheme2, LoadingState } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime'; import { PanelRenderer } from '@grafana/runtime';
import { TableCellDisplayMode, useStyles2 } from '@grafana/ui'; import { TableCellDisplayMode, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { PreviewRuleResponse } from '../../types/preview'; import { PreviewRuleResponse } from '../../types/preview';
import { RuleFormType } from '../../types/rule-form'; import { RuleFormType } from '../../types/rule-form';
@@ -36,7 +37,9 @@ export function PreviewRuleResult(props: Props): React.ReactElement | null {
if (data.state === LoadingState.Loading) { if (data.state === LoadingState.Loading) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<span>Loading preview...</span> <span>
<Trans i18nKey="alerting.preview-rule-result.loading-preview">Loading preview...</Trans>
</span>
</div> </div>
); );
} }
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { GrafanaTheme2, RelativeTimeRange, dateTime, getDefaultRelativeTimeRange, rangeUtil } from '@grafana/data'; import { GrafanaTheme2, RelativeTimeRange, dateTime, getDefaultRelativeTimeRange, rangeUtil } from '@grafana/data';
import { Icon, InlineField, RelativeTimeRangePicker, Toggletip, clearButtonStyles, useStyles2 } from '@grafana/ui'; import { Icon, InlineField, RelativeTimeRangePicker, Toggletip, clearButtonStyles, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertQuery } from 'app/types/unified-alerting-dto';
import { AlertQueryOptions, MaxDataPointsOption, MinIntervalOption } from './QueryWrapper'; import { AlertQueryOptions, MaxDataPointsOption, MinIntervalOption } from './QueryWrapper';
@@ -34,7 +35,7 @@ export const QueryOptions = ({
content={ content={
<div className={styles.queryOptions}> <div className={styles.queryOptions}>
{onChangeTimeRange && ( {onChangeTimeRange && (
<InlineField label="Time Range"> <InlineField label={t('alerting.query-options.label-time-range', 'Time Range')}>
<RelativeTimeRangePicker <RelativeTimeRangePicker
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()} timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
onChange={(range) => onChangeTimeRange(range, index)} onChange={(range) => onChangeTimeRange(range, index)}
@@ -14,6 +14,7 @@ import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { Button, Card, Icon, Stack } from '@grafana/ui'; import { Button, Card, Icon, Stack } from '@grafana/ui';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { Trans } from 'app/core/internationalization';
import { isExpressionQuery } from 'app/features/expressions/guards'; import { isExpressionQuery } from 'app/features/expressions/guards';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
@@ -283,7 +284,11 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
<EmptyQueryWrapper> <EmptyQueryWrapper>
<QueryOperationRow title={refId} draggable index={index} id={refId} isOpen collapsable={false}> <QueryOperationRow title={refId} draggable index={index} id={refId} isOpen collapsable={false}>
<Card> <Card>
<Card.Heading>This datasource has been removed</Card.Heading> <Card.Heading>
<Trans i18nKey="alerting.datasource-not-found.this-datasource-has-been-removed">
This datasource has been removed
</Trans>
</Card.Heading>
<Card.Description> <Card.Description>
The datasource for this query was not found, it was either removed or is not installed correctly. The datasource for this query was not found, it was either removed or is not installed correctly.
</Card.Description> </Card.Description>
@@ -292,10 +297,10 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
</Card.Figure> </Card.Figure>
<Card.Actions> <Card.Actions>
<Button key="update" variant="secondary" onClick={handleUpdateDatasource}> <Button key="update" variant="secondary" onClick={handleUpdateDatasource}>
Update datasource <Trans i18nKey="alerting.datasource-not-found.update-datasource">Update datasource</Trans>
</Button> </Button>
<Button key="remove" variant="destructive" onClick={onRemoveQuery}> <Button key="remove" variant="destructive" onClick={onRemoveQuery}>
Remove query <Trans i18nKey="alerting.datasource-not-found.remove-query">Remove query</Trans>
</Button> </Button>
</Card.Actions> </Card.Actions>
<Card.SecondaryActions> <Card.SecondaryActions>
@@ -306,7 +311,7 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
fill="text" fill="text"
size="sm" size="sm"
> >
Show details <Trans i18nKey="alerting.datasource-not-found.show-details">Show details</Trans>
</Button> </Button>
</Card.SecondaryActions> </Card.SecondaryActions>
</Card> </Card>
@@ -19,6 +19,7 @@ import {
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { GraphThresholdsStyleMode, Icon, InlineField, Input, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { GraphThresholdsStyleMode, Icon, InlineField, Input, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { logInfo } from 'app/features/alerting/unified/Analytics'; import { logInfo } from 'app/features/alerting/unified/Analytics';
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
@@ -254,7 +255,7 @@ export function MaxDataPointsOption({
return ( return (
<InlineField <InlineField
labelWidth={24} labelWidth={24}
label="Max data points" label={t('alerting.max-data-points-option.label-max-data-points', 'Max data points')}
tooltip="The maximum data points per series. Used directly by some data sources and used in calculation of auto interval. With streaming data this value is used for the rolling buffer." tooltip="The maximum data points per series. Used directly by some data sources and used in calculation of auto interval. With streaming data this value is used for the rolling buffer."
> >
<Input <Input
@@ -290,7 +291,7 @@ export function MinIntervalOption({
return ( return (
<InlineField <InlineField
label="Interval" label={t('alerting.min-interval-option.label-interval', 'Interval')}
labelWidth={24} labelWidth={24}
tooltip={ tooltip={
<> <>
@@ -1,5 +1,7 @@
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { t } from 'app/core/internationalization';
import { RuleFormValues } from '../../types/rule-form'; import { RuleFormValues } from '../../types/rule-form';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
@@ -18,7 +20,10 @@ export function RecordingRulesNameSpaceAndGroupStep() {
<RuleEditorSection <RuleEditorSection
stepNo={3} stepNo={3}
title={'Add namespace and group'} title={'Add namespace and group'}
description="Select the Namespace and Group for your recording rule." description={t(
'alerting.recording-rules-name-space-and-group-step.description-select-namespace-group-recording',
'Select the Namespace and Group for your recording rule.'
)}
> >
<GroupAndNamespaceFields rulesSourceName={dataSourceName} /> <GroupAndNamespaceFields rulesSourceName={dataSourceName} />
</RuleEditorSection> </RuleEditorSection>
@@ -5,6 +5,7 @@ import { ReactElement } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { FieldSet, InlineSwitch, Stack, Text, useStyles2 } from '@grafana/ui'; import { FieldSet, InlineSwitch, Stack, Text, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
export interface RuleEditorSectionProps { export interface RuleEditorSectionProps {
title: string; title: string;
@@ -44,7 +45,7 @@ export const RuleEditorSection = ({
onChange={(event) => { onChange={(event) => {
switchMode.setAdvancedMode(event.currentTarget.checked); switchMode.setAdvancedMode(event.currentTarget.checked);
}} }}
label="Advanced options" label={t('alerting.rule-editor-section.label-advanced-options', 'Advanced options')}
showLabel showLabel
transparent transparent
className={styles.reverse} className={styles.reverse}

Some files were not shown because too many files have changed in this diff Show More