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:
+121
-851
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 { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types';
|
||||
@@ -55,7 +56,9 @@ const AlertGroups = () => {
|
||||
return (
|
||||
<>
|
||||
<AlertGroupFilter groups={results} />
|
||||
{loading && <LoadingPlaceholder text="Loading notifications" />}
|
||||
{loading && (
|
||||
<LoadingPlaceholder text={t('alerting.alert-groups.text-loading-notifications', 'Loading notifications')} />
|
||||
)}
|
||||
{error && !loading && (
|
||||
<Alert title={'Error loading notifications'} severity={'error'}>
|
||||
{error.message || 'Unknown error'}
|
||||
@@ -63,7 +66,12 @@ const AlertGroups = () => {
|
||||
)}
|
||||
|
||||
{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
|
||||
here for the selected Alertmanager.
|
||||
</Alert>
|
||||
@@ -85,7 +93,11 @@ const AlertGroups = () => {
|
||||
</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 { Alert, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
interface AlertWarningProps {
|
||||
title: string;
|
||||
@@ -12,7 +13,9 @@ export function AlertWarning({ title, children }: AlertWarningProps) {
|
||||
return (
|
||||
<Alert className={useStyles2(warningStyles).warning} severity="warning" title={title}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDebounce } from 'react-use';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui';
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { getQueryParamValue } from 'app/core/utils/query';
|
||||
import { FolderState, useDispatch } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
@@ -66,7 +67,10 @@ export const AlertsFolderView = ({ folder }: Props) => {
|
||||
<FilterInput
|
||||
value={nameFilter}
|
||||
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"
|
||||
/>
|
||||
<Stack direction="row">
|
||||
@@ -75,14 +79,17 @@ export const AlertsFolderView = ({ folder }: Props) => {
|
||||
onChange={({ value }) => value && setSortOrder(value)}
|
||||
options={sortOptions}
|
||||
width={25}
|
||||
aria-label="Sort"
|
||||
aria-label={t('alerting.alerts-folder-view.aria-label-sort', 'Sort')}
|
||||
placeholder={`Sort (Default A-Z)`}
|
||||
prefix={<Icon name={sortOrder === SortOrder.Ascending ? 'sort-amount-up' : 'sort-amount-down'} />}
|
||||
/>
|
||||
<FilterInput
|
||||
value={labelFilter}
|
||||
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}
|
||||
data-testid="label-filter"
|
||||
/>
|
||||
@@ -111,7 +118,11 @@ export const AlertsFolderView = ({ folder }: Props) => {
|
||||
</Card>
|
||||
))}
|
||||
</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}>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { config } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview';
|
||||
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 { AlertDataQuery, AlertQuery } from '../../../types/unified-alerting-dto';
|
||||
@@ -244,7 +245,12 @@ function ExpressionPreview({ refId, model, evalData, isAlertCondition }: Express
|
||||
>
|
||||
<div className={styles.previewWrapper}>
|
||||
{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}
|
||||
</Alert>
|
||||
))}
|
||||
@@ -272,12 +278,14 @@ function QueryBox({ refId, headerItems = [], children, isAlertCondition, explore
|
||||
<span className={styles.refId}>{refId}</span>
|
||||
{headerItems}
|
||||
<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 && (
|
||||
<WithReturnButton
|
||||
component={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
@@ -339,7 +347,9 @@ function ClassicConditionViewer({ model }: { model: ExpressionQuery }) {
|
||||
{index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.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.blue}>{evalFunctions[evaluator.type].text}</div>
|
||||
<div className={styles.bold}>
|
||||
@@ -372,13 +382,19 @@ function ReduceConditionViewer({ model }: { model: ExpressionQuery }) {
|
||||
|
||||
return (
|
||||
<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.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.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>
|
||||
);
|
||||
@@ -407,16 +423,24 @@ function ResampleExpressionViewer({ model }: { model: ExpressionQuery }) {
|
||||
|
||||
return (
|
||||
<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.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.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.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>
|
||||
);
|
||||
@@ -450,7 +474,9 @@ function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{evaluator && (
|
||||
@@ -465,7 +491,9 @@ function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) {
|
||||
<div className={styles.container}>
|
||||
{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>
|
||||
|
||||
<>
|
||||
@@ -507,7 +535,9 @@ function MathExpressionViewer({ model }: { model: ExpressionQuery }) {
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
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 { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
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 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) => (
|
||||
<div key={index}>Failed to load Grafana rules state: {stringifyErrorLike(error)}</div>
|
||||
))}
|
||||
@@ -42,7 +45,7 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
|
||||
return (
|
||||
<div className={styles.innerWrapper}>
|
||||
{alert}
|
||||
<LoadingPlaceholder text="Loading rules..." />
|
||||
<LoadingPlaceholder text={t('alerting.panel-alert-tab-content.text-loading-rules', 'Loading rules...')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +80,10 @@ export const PanelAlertTabContent = ({ dashboard, panel }: Props) => {
|
||||
</>
|
||||
)}
|
||||
{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">
|
||||
Dashboard must be saved before alerts can be added.
|
||||
</Trans>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useLocation } from 'react-use';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, isFetchError } from '@grafana/runtime';
|
||||
import { Alert, Card, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { AlertLabels } from './components/AlertLabels';
|
||||
import { RuleViewerLayout } from './components/rule-viewer/RuleViewerLayout';
|
||||
@@ -76,7 +77,7 @@ export function RedirectToRuleViewer(): JSX.Element | null {
|
||||
if (loading) {
|
||||
return (
|
||||
<RuleViewerLayout title={pageTitle}>
|
||||
<LoadingPlaceholder text="Loading rule..." />
|
||||
<LoadingPlaceholder text={t('alerting.redirect-to-rule-viewer.text-loading-rule', 'Loading rule...')} />
|
||||
</RuleViewerLayout>
|
||||
);
|
||||
}
|
||||
@@ -86,7 +87,7 @@ export function RedirectToRuleViewer(): JSX.Element | null {
|
||||
if (!rulesSource) {
|
||||
return (
|
||||
<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>
|
||||
</Alert>
|
||||
</RuleViewerLayout>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LinkButton, Stack, Text } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { WithReturnButton } from './components/WithReturnButton';
|
||||
@@ -27,10 +28,10 @@ function SettingsContent() {
|
||||
actions={[
|
||||
<WithReturnButton
|
||||
key="add-alertmanager"
|
||||
title="Alerting settings"
|
||||
title={t('alerting.settings-content.title-alerting-settings', 'Alerting settings')}
|
||||
component={
|
||||
<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>
|
||||
}
|
||||
/>,
|
||||
@@ -38,10 +39,14 @@ function SettingsContent() {
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{/* 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} />
|
||||
{/* 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} />
|
||||
</Stack>
|
||||
{configurationDrawer}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
interface Props {
|
||||
labelKey: string;
|
||||
@@ -18,7 +19,14 @@ export const AlertLabel = ({ labelKey, value, operator = '=', onRemoveLabel }: P
|
||||
{labelKey}
|
||||
{operator}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size, onClick }: Props)
|
||||
const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels');
|
||||
|
||||
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]) => {
|
||||
return (
|
||||
<Label
|
||||
|
||||
+14
-2
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
@@ -40,7 +41,12 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
|
||||
|
||||
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.External) {
|
||||
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
|
||||
will not affect delivery of your alerts.
|
||||
<div className={styles.adminHint}>
|
||||
@@ -53,7 +59,13 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf
|
||||
|
||||
if (amChoiceStatus.alertmanagersChoice === AlertmanagerChoice.All && hasActiveExternalAMs) {
|
||||
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
|
||||
will not affect the others.
|
||||
<div className={styles.adminHint}>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
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
|
||||
Grafana.
|
||||
<br />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { AlertManagerDataSource } from '../utils/datasource';
|
||||
|
||||
@@ -7,13 +8,22 @@ interface Props {
|
||||
}
|
||||
|
||||
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.
|
||||
</Alert>
|
||||
);
|
||||
|
||||
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
|
||||
Alertmanager from the dropdown.
|
||||
</Alert>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
import { Alert, Badge, Tooltip } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
export enum ProvisionedResource {
|
||||
ContactPoint = 'contact point',
|
||||
@@ -37,7 +37,7 @@ export const ProvisioningBadge = ({
|
||||
*/
|
||||
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) {
|
||||
const provenanceTooltip = (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertState, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@@ -41,7 +42,7 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
|
||||
icon={'bell'}
|
||||
size={'sm'}
|
||||
>
|
||||
Manage silences
|
||||
<Trans i18nKey="alerting.alert-details.manage-silences">Manage silences</Trans>
|
||||
</LinkButton>
|
||||
</Authorize>
|
||||
)}
|
||||
@@ -53,7 +54,7 @@ export const AlertDetails = ({ alert, alertManagerSourceName }: AmNotificationsA
|
||||
icon={'bell-slash'}
|
||||
size={'sm'}
|
||||
>
|
||||
Silence
|
||||
<Trans i18nKey="alerting.alert-details.silence">Silence</Trans>
|
||||
</LinkButton>
|
||||
</Authorize>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack, TextLink, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { createContactPointSearchLink } from '../../utils/misc';
|
||||
@@ -55,7 +56,9 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<span>No grouping</span>
|
||||
<span>
|
||||
<Trans i18nKey="alerting.alert-group.no-grouping">No grouping</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<AlertGroupHeader group={group} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { AlertState, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { getFiltersFromUrlParams } from '../../utils/misc';
|
||||
@@ -55,7 +56,7 @@ export const AlertGroupFilter = ({ groups }: Props) => {
|
||||
/>
|
||||
{showClearButton && (
|
||||
<Button className={styles.clearButton} variant={'secondary'} icon="times" onClick={clearFilters}>
|
||||
Clear filters
|
||||
<Trans i18nKey="alerting.alert-group-filter.clear-filters">Clear filters</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Icon, Label, RadioButtonGroup, Tooltip } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
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
|
||||
managed.
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { uniq } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Icon, Label, MultiSelect, Tooltip } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { isPrivateLabelKey } from '../../utils/labels';
|
||||
@@ -38,7 +39,7 @@ export const GroupBy = ({ groups, groupBy, onGroupingChange }: Props) => {
|
||||
<MultiSelect
|
||||
aria-label={'group by label keys'}
|
||||
value={groupBy}
|
||||
placeholder="Group by"
|
||||
placeholder={t('alerting.group-by.placeholder-group-by', 'Group by')}
|
||||
prefix={<Icon name={'tag-alt'} />}
|
||||
onChange={(items) => {
|
||||
onGroupingChange(items.map(({ value }) => value as string));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useDebounce } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { LogMessages, logInfo } from '../../Analytics';
|
||||
import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
|
||||
@@ -47,7 +48,9 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
|
||||
label={
|
||||
<Label>
|
||||
<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
|
||||
content={
|
||||
<div>
|
||||
@@ -70,7 +73,7 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
placeholder={t('alerting.matcher-filter.search-query-input-placeholder-search', 'Search')}
|
||||
value={filterQuery}
|
||||
onChange={(e) => setFilterQuery(e.currentTarget.value)}
|
||||
data-testid="search-query-input"
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { MultiSelect, MultiSelectCommonProps } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { useSelectableMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
|
||||
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
|
||||
import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager';
|
||||
@@ -22,9 +23,9 @@ const MuteTimingsSelector = ({
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
aria-label="Mute timings"
|
||||
aria-label={t('alerting.mute-timings-selector.aria-label-mute-timings', 'Mute timings')}
|
||||
options={muteTimingOptions}
|
||||
placeholder="Select mute timings..."
|
||||
placeholder={t('alerting.mute-timings-selector.placeholder-select-mute-timings', 'Select mute timings...')}
|
||||
{...selectProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config';
|
||||
@@ -25,19 +26,19 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P
|
||||
<>
|
||||
{loading === true && (
|
||||
<Button icon="fire" size="sm" type="button" disabled>
|
||||
Declare Incident
|
||||
<Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{installed === false && (
|
||||
<Tooltip content={'Grafana Incident is not installed or is not configured correctly'}>
|
||||
<Button icon="fire" size="sm" type="button" disabled>
|
||||
Declare Incident
|
||||
<Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{settings && (
|
||||
<LinkButton icon="fire" size="sm" type="button" href={bridgeURL}>
|
||||
Declare Incident
|
||||
<Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
</>
|
||||
@@ -55,13 +56,29 @@ export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }:
|
||||
|
||||
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 && (
|
||||
<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>
|
||||
)}
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-3
@@ -86,7 +86,11 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
if (showManagePermissions) {
|
||||
menuActions.push(
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +100,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
<Fragment key="export-contact-point">
|
||||
<Menu.Item
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
label={t('alerting.contact-point-header.export-label-export', 'Export')}
|
||||
ariaLabel="export"
|
||||
disabled={!exportAllowed}
|
||||
data-testid="export"
|
||||
@@ -155,7 +159,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
label="Delete"
|
||||
label={t('alerting.contact-point-header.label-delete', 'Delete')}
|
||||
ariaLabel="delete"
|
||||
icon="trash-alt"
|
||||
destructive
|
||||
|
||||
@@ -72,7 +72,7 @@ const ContactPointsTab = () => {
|
||||
const search = queryParams.get('search');
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text="Loading..." />;
|
||||
return <LoadingPlaceholder text={t('alerting.contact-points-tab.text-loading', 'Loading...')} />;
|
||||
}
|
||||
|
||||
const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
@@ -107,7 +107,7 @@ const ContactPointsTab = () => {
|
||||
{addContactPointSupported && (
|
||||
<LinkButton
|
||||
icon="plus"
|
||||
aria-label="add contact point"
|
||||
aria-label={t('alerting.contact-points-tab.aria-label-add-contact-point', 'add contact point')}
|
||||
variant="primary"
|
||||
href="/alerting/notifications/receivers/new"
|
||||
disabled={!addContactPointAllowed}
|
||||
@@ -119,17 +119,24 @@ const ContactPointsTab = () => {
|
||||
<Button
|
||||
icon="download-alt"
|
||||
variant="secondary"
|
||||
aria-label="export all"
|
||||
aria-label={t('alerting.contact-points-tab.aria-label-export-all', 'export all')}
|
||||
disabled={!exportContactPointsAllowed}
|
||||
onClick={() => showExportDrawer(ALL_CONTACT_POINTS)}
|
||||
>
|
||||
Export all
|
||||
<Trans i18nKey="alerting.contact-points-tab.export-all">Export all</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{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} />
|
||||
)}
|
||||
@@ -149,7 +156,9 @@ const NotificationTemplatesTab = () => {
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<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>
|
||||
{createTemplateSupported && (
|
||||
<LinkButton
|
||||
@@ -158,7 +167,9 @@ const NotificationTemplatesTab = () => {
|
||||
href="/alerting/notifications/templates/new"
|
||||
disabled={!createTemplateAllowed}
|
||||
>
|
||||
Add notification template group
|
||||
<Trans i18nKey="alerting.notification-templates-tab.add-notification-template-group">
|
||||
Add notification template group
|
||||
</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -211,7 +222,7 @@ export const ContactPointsPageContents = () => {
|
||||
<TabsBar>
|
||||
{showContactPointsTab && (
|
||||
<Tab
|
||||
label="Contact Points"
|
||||
label={t('alerting.contact-points-page-contents.label-contact-points', 'Contact Points')}
|
||||
active={showingContactPoints}
|
||||
counter={contactPoints.length}
|
||||
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
|
||||
@@ -219,7 +230,7 @@ export const ContactPointsPageContents = () => {
|
||||
)}
|
||||
{showTemplatesTab && (
|
||||
<Tab
|
||||
label="Notification Templates"
|
||||
label={t('alerting.contact-points-page-contents.label-notification-templates', 'Notification Templates')}
|
||||
active={showNotificationTemplates}
|
||||
onChangeTab={() => setActiveTab(ActiveTab.NotificationTemplates)}
|
||||
/>
|
||||
|
||||
+15
-2
@@ -48,14 +48,27 @@ const DuplicateMessageTemplateComponent = () => {
|
||||
}
|
||||
|
||||
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) {
|
||||
return isNotFoundError(error) ? (
|
||||
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)}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { useGetContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
||||
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
|
||||
@@ -21,12 +22,15 @@ const EditContactPoint = () => {
|
||||
} = useGetContactPoint({ name: contactPointName, alertmanager: selectedAlertmanager! });
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text="Loading..." />;
|
||||
return <LoadingPlaceholder text={t('alerting.edit-contact-point.text-loading', 'Loading...')} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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)}
|
||||
</Alert>
|
||||
);
|
||||
@@ -34,7 +38,7 @@ const EditContactPoint = () => {
|
||||
|
||||
if (!contactPoint) {
|
||||
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.'}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
+10
-2
@@ -32,14 +32,22 @@ const EditMessageTemplateComponent = () => {
|
||||
}
|
||||
|
||||
if (isLoading || isUninitialized) {
|
||||
return <LoadingPlaceholder text="Loading template..." />;
|
||||
return (
|
||||
<LoadingPlaceholder text={t('alerting.edit-message-template.text-loading-template', 'Loading template...')} />
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return isNotFoundError(error) ? (
|
||||
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)}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
+19
-2
@@ -1,4 +1,5 @@
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { stringifyErrorLike } from '../../utils/misc';
|
||||
@@ -11,11 +12,27 @@ export const NotificationTemplates = () => {
|
||||
const { data: templates, isLoading, error } = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' });
|
||||
|
||||
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) {
|
||||
return <LoadingPlaceholder text="Loading notification templates" />;
|
||||
return (
|
||||
<LoadingPlaceholder
|
||||
text={t(
|
||||
'alerting.notification-templates.text-loading-notification-templates',
|
||||
'Loading notification templates'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (templates) {
|
||||
|
||||
+15
-5
@@ -3,6 +3,7 @@ import { useCallback, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
|
||||
|
||||
@@ -32,10 +33,13 @@ const ContactPointsFilter = () => {
|
||||
|
||||
return (
|
||||
<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
|
||||
aria-label="search contact points"
|
||||
placeholder="Search"
|
||||
aria-label={t('alerting.contact-points-filter.aria-label-search-contact-points', 'search contact points')}
|
||||
placeholder={t('alerting.contact-points-filter.placeholder-search', 'Search')}
|
||||
width={46}
|
||||
prefix={<Icon name="search" />}
|
||||
onChange={(event) => {
|
||||
@@ -44,8 +48,14 @@ const ContactPointsFilter = () => {
|
||||
value={searchValue}
|
||||
/>
|
||||
</Field>
|
||||
<Button variant="secondary" icon="times" onClick={() => clear()} disabled={!hasInput} aria-label="clear">
|
||||
Clear
|
||||
<Button
|
||||
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>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+8
-1
@@ -1,4 +1,5 @@
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig';
|
||||
import { useAlertmanager } from '../../../state/AlertmanagerContext';
|
||||
@@ -16,7 +17,13 @@ const GlobalConfig = () => {
|
||||
|
||||
if (error) {
|
||||
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)}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
+8
-1
@@ -1,4 +1,5 @@
|
||||
import { Alert, LinkButton } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { AlertmanagerAction } from '../../../hooks/useAbilities';
|
||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||
@@ -14,7 +15,13 @@ export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps)
|
||||
|
||||
return (
|
||||
<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>
|
||||
For each external Alertmanager you can define global settings, like server addresses, usernames and password,
|
||||
for all the supported contact points.
|
||||
|
||||
+20
-5
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
@@ -54,10 +54,21 @@ export const useDeleteContactPointModal = (
|
||||
onDismiss={handleDismiss}
|
||||
closeOnBackdropClick={!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>Are you sure you want to delete this contact point?</p>
|
||||
<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>
|
||||
<Button type="button" variant="destructive" onClick={handleSubmit} disabled={isLoading}>
|
||||
@@ -85,7 +96,11 @@ const ErrorModal = ({ isOpen, onDismiss, error }: ErrorModalProps) => (
|
||||
closeOnEscape={true}
|
||||
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>
|
||||
<code>{stringifyErrorLike(error)}</code>
|
||||
</pre>
|
||||
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
export const UnusedContactPointBadge = () => (
|
||||
<Badge
|
||||
text="Unused"
|
||||
aria-label="unused"
|
||||
text={t('alerting.unused-contact-point-badge.text-unused', 'Unused')}
|
||||
aria-label={t('alerting.unused-contact-point-badge.aria-label-unused', 'unused')}
|
||||
color="orange"
|
||||
icon="exclamation-triangle"
|
||||
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>
|
||||
</Button>
|
||||
<ClipboardButton icon="copy" getText={() => textDefinition}>
|
||||
Copy code
|
||||
<Trans i18nKey="alerting.file-export-preview.copy-code">Copy code</Trans>
|
||||
</ClipboardButton>
|
||||
<Button icon="download-alt" onClick={onDownload}>
|
||||
Download
|
||||
<Trans i18nKey="alerting.file-export-preview.download">Download</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { RuleIdentifier } from '../../../../../types/unified-alerting';
|
||||
import { useRuleWithLocation } from '../../hooks/useCombinedRule';
|
||||
@@ -23,7 +24,7 @@ function GrafanaModifyExport() {
|
||||
|
||||
if (!ruleIdentifier) {
|
||||
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.
|
||||
</Alert>
|
||||
);
|
||||
@@ -36,12 +37,15 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
|
||||
const { loading, error, result: rulerRule } = useRuleWithLocation({ ruleIdentifier: ruleIdentifier });
|
||||
|
||||
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) {
|
||||
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)}
|
||||
</Alert>
|
||||
);
|
||||
@@ -51,7 +55,7 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
|
||||
// alert rule does not exist
|
||||
return (
|
||||
<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"
|
||||
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
|
||||
return (
|
||||
<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"
|
||||
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() {
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -19,7 +20,9 @@ const GrafanaMuteTimingsExporterPreview = ({ exportFormat, onClose }: MuteTiming
|
||||
const downloadFileName = `mute-timings-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return (
|
||||
<LoadingPlaceholder text={t('alerting.grafana-mute-timings-exporter-preview.text-loading', 'Loading....')} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FileExportPreview
|
||||
@@ -46,7 +49,7 @@ const GrafanaMuteTimingExporterPreview = ({
|
||||
const downloadFileName = `mute-timing-${muteTimingName}-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-mute-timing-exporter-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
return (
|
||||
<FileExportPreview
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -20,7 +21,7 @@ const GrafanaPoliciesExporterPreview = ({ exportFormat, onClose }: GrafanaPolici
|
||||
const downloadFileName = `policies-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-policies-exporter-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -30,7 +31,7 @@ const GrafanaReceiverExportPreview = ({
|
||||
const downloadFileName = `cp-${receiverName}-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-receiver-export-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -23,7 +24,7 @@ const GrafanaReceiversExportPreview = ({ decrypt, exportFormat, onClose }: Grafa
|
||||
const downloadFileName = `contact-points-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-receivers-export-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -23,7 +24,7 @@ const GrafanaRuleExportPreview = ({ alertUid, exportFormat, onClose }: GrafanaRu
|
||||
const downloadFileName = `${alertUid}-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-rule-export-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { FolderDTO } from '../../../../../types';
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
@@ -43,7 +44,7 @@ function GrafanaRuleFolderExportPreview({ folder, exportFormat, onClose }: Grafa
|
||||
});
|
||||
|
||||
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}`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -55,7 +56,7 @@ function GrafanaRuleGroupExportPreview({
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-rule-group-export-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
@@ -40,7 +41,7 @@ function GrafanaRulesExportPreview({ exportFormat, onClose }: GrafanaRulesExport
|
||||
const downloadFileName = `alert-rules-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
return <LoadingPlaceholder text={t('alerting.grafana-rules-export-preview.text-loading', 'Loading....')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
isTimeSeriesFrames,
|
||||
} from '@grafana/data';
|
||||
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 { Math } from 'app/features/expressions/components/Math';
|
||||
import { Reduce } from 'app/features/expressions/components/Reduce';
|
||||
@@ -164,12 +165,12 @@ export const Expression: FC<ExpressionProps> = ({
|
||||
/>
|
||||
<div className={styles.expression.body}>
|
||||
{error && (
|
||||
<Alert title="Expression failed" severity="error">
|
||||
<Alert title={t('alerting.expression.title-expression-failed', 'Expression failed')} severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{warning && (
|
||||
<Alert title="Expression warning" severity="warning">
|
||||
<Alert title={t('alerting.expression.title-expression-warning', 'Expression warning')} severity="warning">
|
||||
{warning.message}
|
||||
</Alert>
|
||||
)}
|
||||
@@ -246,7 +247,11 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
|
||||
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 && (
|
||||
<div className={styles.pagination.wrapper} data-testid="paginate-expression">
|
||||
<Stack>
|
||||
@@ -256,7 +261,7 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
|
||||
onClick={previousPage}
|
||||
icon="angle-left"
|
||||
size="sm"
|
||||
aria-label="previous-page"
|
||||
aria-label={t('alerting.expression-result.aria-label-previouspage', 'previous-page')}
|
||||
/>
|
||||
<Spacer />
|
||||
<span className={styles.mutedText}>
|
||||
@@ -269,7 +274,7 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
|
||||
onClick={nextPage}
|
||||
icon="angle-right"
|
||||
size="sm"
|
||||
aria-label="next-page"
|
||||
aria-label={t('alerting.expression-result.aria-label-nextpage', 'next-page')}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -287,7 +292,11 @@ export const PreviewSummary: FC<{ firing: number; normal: number; isCondition: b
|
||||
const { mutedText } = useStyles2(getStyles);
|
||||
|
||||
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) {
|
||||
@@ -478,8 +487,12 @@ const TimeseriesRow: FC<TimeseriesRowProps & { index: number }> = ({ frame, inde
|
||||
<table className={styles.timeseriesTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.timeseries-row.timestamp">Timestamp</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.timeseries-row.value">Value</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -493,7 +506,9 @@ const TimeseriesRow: FC<TimeseriesRowProps & { index: number }> = ({ frame, inde
|
||||
</table>
|
||||
}
|
||||
>
|
||||
<span>Time series data</span>
|
||||
<span>
|
||||
<Trans i18nKey="alerting.timeseries-row.time-series-data">Time series data</Trans>
|
||||
</span>
|
||||
</PopupCard>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@ import { ReactElement, useMemo, useState } from 'react';
|
||||
import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data';
|
||||
import { usePluginLinks } from '@grafana/runtime';
|
||||
import { Dropdown, IconButton } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
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.
|
||||
import { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu';
|
||||
@@ -31,7 +32,11 @@ export const AlertInstanceExtensionPoint = ({
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{!!selectedExtension && !!selectedExtension.path && (
|
||||
<ConfirmNavigationModal
|
||||
|
||||
+4
-2
@@ -52,7 +52,9 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{exportSupported && (
|
||||
@@ -84,7 +86,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
|
||||
</Stack>
|
||||
<ConfirmModal
|
||||
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}"?`}
|
||||
confirmText={t('alerting.common.delete', 'Delete')}
|
||||
onConfirm={async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
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 {
|
||||
MuteTiming,
|
||||
useCreateMuteTiming,
|
||||
@@ -88,11 +88,15 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
|
||||
};
|
||||
|
||||
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) {
|
||||
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 (
|
||||
@@ -103,8 +107,11 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
|
||||
<FieldSet disabled={provisioned || updating}>
|
||||
<Field
|
||||
required
|
||||
label="Name"
|
||||
description="A unique name for the mute timing"
|
||||
label={t('alerting.mute-timing-form.label-name', 'Name')}
|
||||
description={t(
|
||||
'alerting.mute-timing-form.description-unique-timing',
|
||||
'A unique name for the mute timing'
|
||||
)}
|
||||
invalid={!!formApi.formState.errors?.name}
|
||||
error={formApi.formState.errors.name?.message}
|
||||
>
|
||||
|
||||
+26
-12
@@ -5,6 +5,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { MuteTimingFields } from '../../types/mute-timing-form';
|
||||
@@ -26,7 +27,7 @@ export const MuteTimingTimeInterval = () => {
|
||||
const { isGrafanaAlertmanager } = useAlertmanager();
|
||||
|
||||
return (
|
||||
<FieldSet label="Time intervals">
|
||||
<FieldSet label={t('alerting.mute-timing-time-interval.label-time-intervals', 'Time intervals')}>
|
||||
<>
|
||||
<p>
|
||||
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}>
|
||||
<MuteTimingTimeRange intervalIndex={timeIntervalIndex} />
|
||||
<Field
|
||||
label="Location"
|
||||
label={t('alerting.mute-timing-time-interval.label-location', 'Location')}
|
||||
invalid={Boolean(errors.time_intervals?.[timeIntervalIndex]?.location)}
|
||||
error={errors.time_intervals?.[timeIntervalIndex]?.location?.message}
|
||||
>
|
||||
@@ -61,7 +62,7 @@ export const MuteTimingTimeInterval = () => {
|
||||
data-testid="mute-timing-location"
|
||||
/>
|
||||
</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
|
||||
onChange={(daysOfWeek) => {
|
||||
setValue(`time_intervals.${timeIntervalIndex}.weekdays`, daysOfWeek);
|
||||
@@ -71,7 +72,7 @@ export const MuteTimingTimeInterval = () => {
|
||||
/>
|
||||
</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"
|
||||
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.days_of_month}
|
||||
error={errors.time_intervals?.[timeIntervalIndex]?.days_of_month?.message}
|
||||
@@ -83,12 +84,15 @@ export const MuteTimingTimeInterval = () => {
|
||||
width={50}
|
||||
// @ts-ignore react-hook-form doesn't handle nested field arrays well
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.months}
|
||||
error={errors.time_intervals?.[timeIntervalIndex]?.months?.message}
|
||||
@@ -103,14 +107,17 @@ export const MuteTimingTimeInterval = () => {
|
||||
),
|
||||
})}
|
||||
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
|
||||
defaultValue={timeInterval.months}
|
||||
data-testid="mute-timing-months"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Years"
|
||||
label={t('alerting.mute-timing-time-interval.label-years', 'Years')}
|
||||
invalid={!!errors.time_intervals?.[timeIntervalIndex]?.years}
|
||||
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'),
|
||||
})}
|
||||
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
|
||||
defaultValue={timeInterval.years}
|
||||
data-testid="mute-timing-years"
|
||||
@@ -133,7 +143,9 @@ export const MuteTimingTimeInterval = () => {
|
||||
icon="trash-alt"
|
||||
onClick={() => removeTimeInterval(timeIntervalIndex)}
|
||||
>
|
||||
Remove time interval
|
||||
<Trans i18nKey="alerting.mute-timing-time-interval.remove-time-interval">
|
||||
Remove time interval
|
||||
</Trans>
|
||||
</Button>
|
||||
{/*
|
||||
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 && (
|
||||
<InlineSwitch
|
||||
id={`time_intervals.${timeIntervalIndex}.disable`}
|
||||
label="Disable"
|
||||
label={t('alerting.mute-timing-time-interval.label-disable', 'Disable')}
|
||||
showLabel
|
||||
transparent
|
||||
{...register(`time_intervals.${timeIntervalIndex}.disable`)}
|
||||
@@ -162,7 +174,9 @@ export const MuteTimingTimeInterval = () => {
|
||||
}}
|
||||
icon="plus"
|
||||
>
|
||||
Add another time interval
|
||||
<Trans i18nKey="alerting.mute-timing-time-interval.add-another-time-interval">
|
||||
Add another time interval
|
||||
</Trans>
|
||||
</Button>
|
||||
</>
|
||||
</FieldSet>
|
||||
|
||||
+9
-8
@@ -3,6 +3,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 ConditionalWrap from '../ConditionalWrap';
|
||||
@@ -35,7 +36,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
||||
<div>
|
||||
<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)"
|
||||
invalid={timeRangeInvalid}
|
||||
>
|
||||
@@ -58,7 +59,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
||||
<div className={styles.timeRange} key={timeRange.id}>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Start time"
|
||||
label={t('alerting.mute-timing-time-range.label-start-time', 'Start time')}
|
||||
invalid={Boolean(timeRangeErrors?.start_time)}
|
||||
error={timeRangeErrors?.start_time?.message}
|
||||
>
|
||||
@@ -86,12 +87,12 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
||||
suffix={<Icon name="clock-nine" />}
|
||||
// @ts-ignore react-hook-form doesn't handle nested field arrays well
|
||||
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"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="End time"
|
||||
label={t('alerting.mute-timing-time-range.label-end-time', 'End time')}
|
||||
invalid={Boolean(timeRangeErrors?.end_time)}
|
||||
error={timeRangeErrors?.end_time?.message}
|
||||
>
|
||||
@@ -118,19 +119,19 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
||||
suffix={<Icon name="clock-nine" />}
|
||||
// @ts-ignore react-hook-form doesn't handle nested field arrays well
|
||||
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"
|
||||
/>
|
||||
</InlineField>
|
||||
<IconButton
|
||||
className={styles.deleteTimeRange}
|
||||
title="Remove"
|
||||
title={t('alerting.mute-timing-time-range.title-remove', 'Remove')}
|
||||
name="trash-alt"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeTimeRange(index);
|
||||
}}
|
||||
tooltip="Remove time range"
|
||||
tooltip={t('alerting.mute-timing-time-range.tooltip-remove-time-range', 'Remove time range')}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
@@ -154,7 +155,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
||||
disabled={isDisabled}
|
||||
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>
|
||||
</ConditionalWrap>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,11 @@ export const MuteTimingsTable = () => {
|
||||
const columns = useColumns(alertManagerSourceName, hideActions);
|
||||
|
||||
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) {
|
||||
@@ -107,7 +111,10 @@ export const MuteTimingsTable = () => {
|
||||
<DynamicTable items={items} cols={columns} pagination={{ itemsPerPage: 25 }} />
|
||||
) : !hideActions ? (
|
||||
<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"
|
||||
buttonIcon="plus"
|
||||
buttonSize="lg"
|
||||
@@ -115,7 +122,11 @@ export const MuteTimingsTable = () => {
|
||||
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>
|
||||
);
|
||||
|
||||
+15
-3
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
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 { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
|
||||
@@ -69,7 +70,15 @@ export const ContactPointSelector = ({
|
||||
|
||||
// TODO error handling
|
||||
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 (
|
||||
@@ -86,8 +95,11 @@ export const ContactPointSelector = ({
|
||||
<IconButton
|
||||
name="sync"
|
||||
onClick={onClickRefresh}
|
||||
aria-label="Refresh contact points"
|
||||
tooltip="Refresh contact points list"
|
||||
aria-label={t('alerting.contact-point-selector.aria-label-refresh-contact-points', 'Refresh contact points')}
|
||||
tooltip={t(
|
||||
'alerting.contact-point-selector.tooltip-refresh-contact-points-list',
|
||||
'Refresh contact points list'
|
||||
)}
|
||||
className={cx(styles.refreshButton, {
|
||||
[styles.loading]: loaderSpinning || isLoading,
|
||||
})}
|
||||
|
||||
+15
-12
@@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
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 { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils';
|
||||
import { RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
@@ -52,7 +53,7 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<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}
|
||||
error={errors.receiver?.message}
|
||||
>
|
||||
@@ -71,24 +72,26 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
name="receiver"
|
||||
rules={{ required: { value: true, message: 'Required.' } }}
|
||||
/>
|
||||
<span>or</span>
|
||||
<span>
|
||||
<Trans i18nKey="alerting.am-root-route-form.or">or</Trans>
|
||||
</span>
|
||||
<Link
|
||||
className={styles.linkText}
|
||||
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>
|
||||
</div>
|
||||
</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."
|
||||
data-testid="am-group-select"
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<MultiSelect
|
||||
aria-label="Group by"
|
||||
aria-label={t('alerting.am-root-route-form.aria-label-group-by', 'Group by')}
|
||||
{...field}
|
||||
allowCustomValue
|
||||
className={styles.input}
|
||||
@@ -108,12 +111,12 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
collapsible
|
||||
className={styles.collapse}
|
||||
isOpen={isTimingOptionsExpanded}
|
||||
label="Timing options"
|
||||
label={t('alerting.am-root-route-form.label-timing-options', 'Timing options')}
|
||||
onToggle={setIsTimingOptionsExpanded}
|
||||
>
|
||||
<div className={styles.timingFormContainer}>
|
||||
<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."
|
||||
invalid={!!errors.groupWaitValue}
|
||||
error={errors.groupWaitValue?.message}
|
||||
@@ -123,11 +126,11 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
{...register('groupWaitValue', { validate: promDurationValidator })}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.group_wait}
|
||||
className={styles.promDurationInput}
|
||||
aria-label="Group wait"
|
||||
aria-label={t('alerting.am-root-route-form.aria-label-group-wait', 'Group wait')}
|
||||
/>
|
||||
</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."
|
||||
invalid={!!errors.groupIntervalValue}
|
||||
error={errors.groupIntervalValue?.message}
|
||||
@@ -137,11 +140,11 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
{...register('groupIntervalValue', { validate: promDurationValidator })}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.group_interval}
|
||||
className={styles.promDurationInput}
|
||||
aria-label="Group interval"
|
||||
aria-label={t('alerting.am-root-route-form.aria-label-group-interval', 'Group interval')}
|
||||
/>
|
||||
</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."
|
||||
invalid={!!errors.repeatIntervalValue}
|
||||
error={errors.repeatIntervalValue?.message}
|
||||
@@ -156,7 +159,7 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
})}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval}
|
||||
className={styles.promDurationInput}
|
||||
aria-label="Repeat interval"
|
||||
aria-label={t('alerting.am-root-route-form.aria-label-repeat-interval', 'Repeat interval')}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
+32
-17
@@ -16,6 +16,7 @@ import {
|
||||
Switch,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector';
|
||||
import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
|
||||
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)}>
|
||||
<input type="hidden" {...register('id')} />
|
||||
<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 && (
|
||||
<Badge
|
||||
color="orange"
|
||||
@@ -101,14 +104,14 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
return (
|
||||
<Stack direction="row" key={field.id} alignItems="center">
|
||||
<Field
|
||||
label="Label"
|
||||
label={t('alerting.am-routes-expanded-form.label-label', 'Label')}
|
||||
invalid={!!errors.object_matchers?.[index]?.name}
|
||||
error={errors.object_matchers?.[index]?.name?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`object_matchers.${index}.name`, { required: 'Field is required' })}
|
||||
defaultValue={field.name}
|
||||
placeholder="label"
|
||||
placeholder={t('alerting.am-routes-expanded-form.placeholder-label', 'label')}
|
||||
autoFocus
|
||||
/>
|
||||
</Field>
|
||||
@@ -120,7 +123,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
className={styles.matchersOperator}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
options={matcherFieldOptions}
|
||||
aria-label="Operator"
|
||||
aria-label={t('alerting.am-routes-expanded-form.aria-label-operator', 'Operator')}
|
||||
/>
|
||||
)}
|
||||
defaultValue={field.operator}
|
||||
@@ -130,18 +133,22 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Value"
|
||||
label={t('alerting.am-routes-expanded-form.label-value', 'Value')}
|
||||
invalid={!!errors.object_matchers?.[index]?.value}
|
||||
error={errors.object_matchers?.[index]?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`object_matchers.${index}.value`)}
|
||||
defaultValue={field.value}
|
||||
placeholder="value"
|
||||
placeholder={t('alerting.am-routes-expanded-form.placeholder-value', 'value')}
|
||||
/>
|
||||
</Field>
|
||||
<IconButton tooltip="Remove matcher" name={'trash-alt'} onClick={() => remove(index)}>
|
||||
Remove
|
||||
<IconButton
|
||||
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>
|
||||
</Stack>
|
||||
);
|
||||
@@ -155,11 +162,11 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
Add matcher
|
||||
<Trans i18nKey="alerting.am-routes-expanded-form.add-matcher">Add matcher</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Field label="Contact point">
|
||||
<Field label={t('alerting.am-routes-expanded-form.label-contact-point', 'Contact point')}>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, value, ...field } }) => (
|
||||
<ContactPointSelector
|
||||
@@ -176,15 +183,20 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
name="receiver"
|
||||
/>
|
||||
</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')} />
|
||||
</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')} />
|
||||
</Field>
|
||||
{watch().overrideGrouping && (
|
||||
<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."
|
||||
>
|
||||
<Controller
|
||||
@@ -199,7 +211,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
|
||||
<>
|
||||
<MultiSelect
|
||||
aria-label="Group by"
|
||||
aria-label={t('alerting.am-routes-expanded-form.aria-label-group-by', 'Group by')}
|
||||
{...field}
|
||||
invalid={Boolean(error)}
|
||||
allowCustomValue
|
||||
@@ -219,7 +231,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
/>
|
||||
</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')} />
|
||||
</Field>
|
||||
{watch().overrideTimings && (
|
||||
@@ -268,9 +280,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
</>
|
||||
)}
|
||||
<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"
|
||||
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}
|
||||
>
|
||||
<Controller
|
||||
|
||||
@@ -3,7 +3,7 @@ import { debounce, isEqual } from 'lodash';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
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 { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
|
||||
import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
@@ -92,7 +92,7 @@ const NotificationPoliciesFilter = ({
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
data-testid="search-query-input"
|
||||
placeholder="Search"
|
||||
placeholder={t('alerting.notification-policies-filter.search-query-input-placeholder-search', 'Search')}
|
||||
width={46}
|
||||
prefix={<Icon name="search" />}
|
||||
onChange={(event) => {
|
||||
@@ -102,7 +102,10 @@ const NotificationPoliciesFilter = ({
|
||||
/>
|
||||
</Field>
|
||||
{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
|
||||
selectProps={{
|
||||
id: 'receiver',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { groupBy } from 'lodash';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
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 { FormAmRoute } from '../../types/amroutes';
|
||||
@@ -52,7 +52,10 @@ const useAddPolicyModal = (
|
||||
onDismiss={handleDismiss}
|
||||
closeOnBackdropClick={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} />}
|
||||
<AmRoutesExpandedForm
|
||||
@@ -116,7 +119,10 @@ const useEditPolicyModal = (
|
||||
onDismiss={handleDismiss}
|
||||
closeOnBackdropClick={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} />}
|
||||
{isDefaultPolicy && route && (
|
||||
@@ -191,7 +197,10 @@ const useDeletePolicyModal = (
|
||||
onDismiss={handleDismiss}
|
||||
closeOnBackdropClick={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} />}
|
||||
<Trans i18nKey="alerting.policies.delete.warning-1">
|
||||
|
||||
+15
-3
@@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { Alert, Button, Stack } from '@grafana/ui';
|
||||
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 { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
|
||||
import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes';
|
||||
@@ -201,13 +201,25 @@ export const NotificationPoliciesList = () => {
|
||||
return (
|
||||
<>
|
||||
{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.'}
|
||||
</Alert>
|
||||
)}
|
||||
{/* show when there is an update error */}
|
||||
{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">
|
||||
<Trans i18nKey="alerting.policies.update-errors.conflict">
|
||||
The notification policy tree has been updated by another user.
|
||||
|
||||
@@ -270,18 +270,18 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label="New sibling above"
|
||||
label={t('alerting.policy.label-new-sibling-above', 'New sibling above')}
|
||||
icon="arrow-up"
|
||||
onClick={() => onAddPolicy(currentRoute, 'above')}
|
||||
/>
|
||||
<Menu.Item
|
||||
label="New sibling below"
|
||||
label={t('alerting.policy.label-new-sibling-below', 'New sibling below')}
|
||||
icon="arrow-down"
|
||||
onClick={() => onAddPolicy(currentRoute, 'below')}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
label="New child policy"
|
||||
label={t('alerting.policy.label-new-child-policy', 'New child policy')}
|
||||
icon="plus"
|
||||
onClick={() => onAddPolicy(currentRoute, 'child')}
|
||||
/>
|
||||
@@ -553,7 +553,7 @@ export const useCreateDropdownMenuActions = (
|
||||
<Menu.Item
|
||||
icon="edit"
|
||||
disabled={provisioned || isAutoGenerated}
|
||||
label="Edit"
|
||||
label={t('alerting.use-create-dropdown-menu-actions.label-edit', 'Edit')}
|
||||
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
@@ -563,7 +563,12 @@ export const useCreateDropdownMenuActions = (
|
||||
|
||||
if (showExportAction) {
|
||||
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
|
||||
icon="trash-alt"
|
||||
disabled={provisioned || isAutoGenerated}
|
||||
label="Delete"
|
||||
label={t('alerting.use-create-dropdown-menu-actions.label-delete', 'Delete')}
|
||||
onClick={() => onDeletePolicy(currentRoute)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
|
||||
+14
-4
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { TimeOptions } from '../../types/time';
|
||||
|
||||
@@ -15,9 +16,15 @@ export function PromDurationDocs() {
|
||||
<hr />
|
||||
<div className={styles.list}>
|
||||
<div className={styles.header}>
|
||||
<div>Symbol</div>
|
||||
<div>Time unit</div>
|
||||
<div>Example</div>
|
||||
<div>
|
||||
<Trans i18nKey="alerting.prom-duration-docs.symbol">Symbol</Trans>
|
||||
</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>
|
||||
<PromDurationDocsTimeUnit unit={TimeOptions.seconds} name="seconds" example="20s" />
|
||||
<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.weeks} name="weeks" example="2w" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-3
@@ -3,6 +3,7 @@ import { useAsync } from 'react-use';
|
||||
|
||||
import { urlUtil } from '@grafana/data';
|
||||
import { Alert, Button, LinkButton } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { useSelector } from 'app/types';
|
||||
@@ -30,12 +31,22 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
|
||||
);
|
||||
|
||||
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) {
|
||||
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.
|
||||
</Alert>
|
||||
);
|
||||
@@ -54,7 +65,7 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
|
||||
className={className}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
+19
-7
@@ -15,7 +15,7 @@ import {
|
||||
clearButtonStyles,
|
||||
useStyles2,
|
||||
} 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 { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
@@ -208,7 +208,7 @@ export function AlertInstanceModalSelector({
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
title="Select alert instances"
|
||||
title={t('alerting.alert-instance-modal-selector.title-select-alert-instances', 'Select alert instances')}
|
||||
className={styles.modal}
|
||||
closeOnEscape
|
||||
isOpen={isOpen}
|
||||
@@ -219,14 +219,19 @@ export function AlertInstanceModalSelector({
|
||||
<FilterInput
|
||||
value={ruleFilter}
|
||||
onChange={handleSearchRules}
|
||||
title="Search alert rule"
|
||||
placeholder="Search alert rule"
|
||||
title={t('alerting.alert-instance-modal-selector.title-search-alert-rule', 'Search alert rule')}
|
||||
placeholder={t('alerting.alert-instance-modal-selector.placeholder-search-alert-rule', 'Search alert rule')}
|
||||
autoFocus
|
||||
/>
|
||||
<div>{(selectedRule && 'Select one or more instances from the list below') || ''}</div>
|
||||
|
||||
<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 && (
|
||||
<AutoSizer>
|
||||
@@ -245,7 +250,12 @@ export function AlertInstanceModalSelector({
|
||||
<div>Select an alert rule to get a list of available firing instances</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 && (
|
||||
<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>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, Button, LinkButton, Stack } from '@grafana/ui';
|
||||
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 { useDispatch } from 'app/types';
|
||||
|
||||
@@ -72,7 +72,10 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => {
|
||||
<FormProvider {...formAPI}>
|
||||
<form onSubmit={handleSubmit(onSubmitCallback)}>
|
||||
{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)}
|
||||
</Alert>
|
||||
)}
|
||||
@@ -92,10 +95,14 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => {
|
||||
<>
|
||||
{loading && (
|
||||
<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>
|
||||
)}
|
||||
{!loading && <Button type="submit">Save global config</Button>}
|
||||
</>
|
||||
)}
|
||||
<LinkButton
|
||||
|
||||
@@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
|
||||
@@ -84,19 +85,22 @@ export function PayloadEditor({
|
||||
<>
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
<EditorColumnHeader
|
||||
label="Payload"
|
||||
label={t('alerting.payload-editor.label-payload', 'Payload')}
|
||||
actions={
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label="Use existing alert instances"
|
||||
label={t(
|
||||
'alerting.payload-editor.label-use-existing-alert-instances',
|
||||
'Use existing alert instances'
|
||||
)}
|
||||
disabled={errorInPayloadJson}
|
||||
onClick={onOpenAlertSelectorModal}
|
||||
/>
|
||||
<Menu.Item
|
||||
label="Add custom alert instance"
|
||||
label={t('alerting.payload-editor.label-add-custom-alert-instance', 'Add custom alert instance')}
|
||||
disabled={errorInPayloadJson}
|
||||
onClick={onOpenEditAlertModal}
|
||||
/>
|
||||
@@ -106,12 +110,12 @@ export function PayloadEditor({
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="sm" icon="angle-down">
|
||||
Edit payload
|
||||
<Trans i18nKey="alerting.payload-editor.edit-payload">Edit payload</Trans>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Toggletip content={<AlertTemplateDataTable />} placement="top" fitContent>
|
||||
<Button variant="secondary" fill="outline" size="sm" icon="question-circle">
|
||||
Reference
|
||||
<Trans i18nKey="alerting.payload-editor.reference">Reference</Trans>
|
||||
</Button>
|
||||
</Toggletip>
|
||||
</Stack>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Dropdown, Icon, Menu, MenuItem, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
|
||||
|
||||
@@ -34,7 +35,16 @@ export const ReceiversSection = ({
|
||||
const showMore = showExport;
|
||||
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 (
|
||||
<Stack direction="column" gap={2}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { PopupCard } from '../HoverCard';
|
||||
|
||||
@@ -83,9 +84,15 @@ export function TemplateDataTable({ dataItems, caption, typeRenderer }: Template
|
||||
{caption && <caption>{caption}</caption>}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Notes</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.template-data-table.name">Name</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.template-data-table.type">Type</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.template-data-table.notes">Notes</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -111,13 +118,23 @@ function KeyValueTemplateDataTable() {
|
||||
<code>{KeyValueCodeSnippet}</code>
|
||||
</pre>
|
||||
<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>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Arguments</th>
|
||||
<th>Returns</th>
|
||||
<th>Notes</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.key-value-template-data-table.name">Name</Trans>
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -167,10 +167,18 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
return (
|
||||
<>
|
||||
<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 && (
|
||||
<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)}
|
||||
</Alert>
|
||||
)}
|
||||
@@ -187,7 +195,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
{/* name and save buttons */}
|
||||
<Stack direction="row" alignItems="center">
|
||||
<InlineField
|
||||
label="Template group name"
|
||||
label={t('alerting.template-form.label-template-group-name', 'Template group name')}
|
||||
error={errors?.title?.message}
|
||||
invalid={!!errors.title?.message}
|
||||
required
|
||||
@@ -197,7 +205,10 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
required: { value: true, message: 'Required.' },
|
||||
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}
|
||||
autoFocus={true}
|
||||
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>
|
||||
<EditorColumnHeader
|
||||
label="Template group"
|
||||
label={t('alerting.template-form.label-template-group', 'Template group')}
|
||||
actions={
|
||||
<>
|
||||
{/* examples dropdown – only available for Grafana Alertmanager */}
|
||||
@@ -334,7 +345,11 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
</form>
|
||||
</FormProvider>
|
||||
{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 />
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Box, Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi';
|
||||
import { stringifyErrorLike } from '../../utils/misc';
|
||||
@@ -44,17 +45,17 @@ export function TemplatePreview({
|
||||
return (
|
||||
<div className={cx(styles.container, className)}>
|
||||
<EditorColumnHeader
|
||||
label="Preview"
|
||||
label={t('alerting.template-preview.label-preview', 'Preview')}
|
||||
actions={
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
icon="sync"
|
||||
aria-label="Refresh preview"
|
||||
aria-label={t('alerting.template-preview.aria-label-refresh-preview', 'Refresh preview')}
|
||||
onClick={onPreview}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Refresh
|
||||
<Trans i18nKey="alerting.template-preview.refresh">Refresh</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -152,7 +153,7 @@ export function getPreviewResults(
|
||||
return (
|
||||
<>
|
||||
{errorToRender && (
|
||||
<Alert severity="error" title="Error">
|
||||
<Alert severity="error" title={t('alerting.get-preview-results.title-error', 'Error')}>
|
||||
{errorToRender}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,9 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Template group</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.templates-table.template-group">Template group</Trans>
|
||||
</th>
|
||||
<Authorize
|
||||
actions={[
|
||||
AlertmanagerAction.CreateNotificationTemplate,
|
||||
@@ -70,14 +72,18 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
|
||||
AlertmanagerAction.DeleteNotificationTemplate,
|
||||
]}
|
||||
>
|
||||
<th>Actions</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.templates-table.actions">Actions</Trans>
|
||||
</th>
|
||||
</Authorize>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!templates.length && (
|
||||
<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>
|
||||
)}
|
||||
{templates.map((notificationTemplate, idx) => (
|
||||
@@ -95,7 +101,7 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
|
||||
{!!templateToDelete && (
|
||||
<ConfirmModal
|
||||
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}"?`}
|
||||
confirmText="Yes, delete"
|
||||
onConfirm={onDeleteTemplate}
|
||||
@@ -153,7 +159,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
{isProvisioned && (
|
||||
<ActionIcon
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
@@ -161,7 +167,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
<Authorize actions={[AlertmanagerAction.UpdateNotificationTemplate]}>
|
||||
<ActionIcon
|
||||
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"
|
||||
/>
|
||||
</Authorize>
|
||||
@@ -172,7 +178,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
`/alerting/notifications/templates/${encodeURIComponent(uid)}/duplicate`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="Copy template group"
|
||||
tooltip={t('alerting.template-row.tooltip-copy-template-group', 'Copy template group')}
|
||||
icon="copy"
|
||||
/>
|
||||
</Authorize>
|
||||
@@ -180,7 +186,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
<Authorize actions={[AlertmanagerAction.DeleteNotificationTemplate]}>
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(notificationTemplate)}
|
||||
tooltip="Delete template group"
|
||||
tooltip={t('alerting.template-row.tooltip-delete-template-group', 'Delete template group')}
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
|
||||
@@ -145,7 +145,11 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
<div className={styles.wrapper} data-testid="item-container">
|
||||
<div className={styles.topRow}>
|
||||
<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
|
||||
name={fieldName('type')}
|
||||
defaultValue={defaultValues.type}
|
||||
@@ -174,13 +178,13 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
onClick={() => handleTest()}
|
||||
icon={testingReceiver ? 'spinner' : 'message'}
|
||||
>
|
||||
Test
|
||||
<Trans i18nKey="alerting.channel-sub-form.test">Test</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{isEditable && (
|
||||
<>
|
||||
<Button size="xs" variant="secondary" type="button" onClick={() => onDuplicate()} icon="copy">
|
||||
Duplicate
|
||||
<Trans i18nKey="alerting.channel-sub-form.duplicate">Duplicate</Trans>
|
||||
</Button>
|
||||
{onDelete && (
|
||||
<Button
|
||||
@@ -191,7 +195,7 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
onClick={() => onDelete()}
|
||||
icon="trash-alt"
|
||||
>
|
||||
Delete
|
||||
<Trans i18nKey="alerting.channel-sub-form.delete">Delete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -244,7 +248,9 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
<CollapsibleSection label="Notification settings">
|
||||
<CollapsibleSection
|
||||
label={t('alerting.channel-sub-form.label-notification-settings', 'Notification settings')}
|
||||
>
|
||||
<CommonSettingsComponent pathPrefix={pathPrefix} readOnly={!isEditable} />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
|
||||
+6
-2
@@ -1,6 +1,7 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Checkbox, Field } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { CommonSettingsComponentProps } from '../../../types/receiver-form';
|
||||
|
||||
@@ -15,9 +16,12 @@ export const CloudCommonChannelSettings = ({
|
||||
<Field disabled={readOnly}>
|
||||
<Checkbox
|
||||
{...register(`${pathPrefix}sendResolved`)}
|
||||
label="Send resolved"
|
||||
label={t('alerting.cloud-common-channel-settings.label-send-resolved', 'Send resolved')}
|
||||
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>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
|
||||
import {
|
||||
useCreateContactPoint,
|
||||
@@ -74,7 +75,7 @@ export const CloudReceiverForm = ({ contactPoint, alertManagerSourceName, readOn
|
||||
return (
|
||||
<>
|
||||
{!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.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
+11
-3
@@ -6,6 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { KeyValueField } from '../../../api/templateApi';
|
||||
@@ -115,7 +116,7 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
|
||||
variant="secondary"
|
||||
disabled={!labelsOrAnnotationsAdded()}
|
||||
>
|
||||
Add alert data
|
||||
<Trans i18nKey="alerting.generate-alert-data-modal.add-alert-data">Add alert data</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
@@ -123,7 +124,12 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
|
||||
<div className={styles.onSubmitWrapper} />
|
||||
{alerts.length > 0 && (
|
||||
<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">
|
||||
{JSON.stringify(alerts, null, 2)}
|
||||
</pre>
|
||||
@@ -132,7 +138,9 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
|
||||
<div className={styles.onSubmitWrapper}>
|
||||
<Modal.ButtonRow>
|
||||
<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>
|
||||
</Modal.ButtonRow>
|
||||
</div>
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Checkbox, Field } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { CommonSettingsComponentProps } from '../../../types/receiver-form';
|
||||
|
||||
@@ -15,7 +16,10 @@ export const GrafanaCommonChannelSettings = ({
|
||||
<Field>
|
||||
<Checkbox
|
||||
{...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"
|
||||
disabled={readOnly}
|
||||
/>
|
||||
|
||||
+11
-2
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import {
|
||||
useCreateContactPoint,
|
||||
useUpdateContactPoint,
|
||||
@@ -142,7 +143,9 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
const isTestable = !readOnly;
|
||||
|
||||
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) => {
|
||||
@@ -158,7 +161,13 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
return (
|
||||
<>
|
||||
{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
|
||||
plugin configuration
|
||||
</Alert>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isFetchError } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, Input, LinkButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
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 { ManagePermissions } from 'app/features/alerting/unified/components/permissions/ManagePermissions';
|
||||
|
||||
@@ -128,7 +128,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
return (
|
||||
<FormProvider {...formAPI}>
|
||||
{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.
|
||||
</Alert>
|
||||
)}
|
||||
@@ -143,11 +143,19 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
resource="receivers"
|
||||
resourceId={contactPointId}
|
||||
resourceName={initialValues?.name}
|
||||
title="Manage contact point permissions"
|
||||
title={t(
|
||||
'alerting.receiver-form.title-manage-contact-point-permissions',
|
||||
'Manage contact point permissions'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
readOnly={!isEditable}
|
||||
id="name"
|
||||
@@ -159,7 +167,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
},
|
||||
})}
|
||||
width={39}
|
||||
placeholder="Name"
|
||||
placeholder={t('alerting.receiver-form.name-placeholder-name', 'Name')}
|
||||
/>
|
||||
</Field>
|
||||
{fields.map((field, index) => {
|
||||
@@ -204,7 +212,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
variant="secondary"
|
||||
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>
|
||||
)}
|
||||
<div className={styles.buttons}>
|
||||
@@ -212,10 +220,14 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
<>
|
||||
{isSubmitting && (
|
||||
<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>
|
||||
)}
|
||||
{!isSubmitting && <Button type="submit">Save contact point</Button>}
|
||||
</>
|
||||
)}
|
||||
<LinkButton
|
||||
|
||||
+7
-2
@@ -4,6 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Annotations, Labels } from 'app/types/unified-alerting-dto';
|
||||
|
||||
@@ -67,7 +68,9 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
return (
|
||||
<Modal onDismiss={onDismiss} isOpen={isOpen} title={'Test contact point'}>
|
||||
<div className={styles.section}>
|
||||
<Label>Notification message</Label>
|
||||
<Label>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.notification-message">Notification message</Trans>
|
||||
</Label>
|
||||
<RadioButtonGroup
|
||||
options={notificationOptions}
|
||||
value={notificationType}
|
||||
@@ -99,7 +102,9 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => {
|
||||
)}
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
+6
-2
@@ -37,8 +37,12 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<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 />}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Input, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { ActionIcon } from '../../../rules/ActionIcon';
|
||||
|
||||
@@ -40,7 +41,7 @@ export const StringArrayInput = ({ value, onChange, readOnly = false }: Props) =
|
||||
<ActionIcon
|
||||
className={styles.deleteIcon}
|
||||
icon="trash-alt"
|
||||
tooltip="delete"
|
||||
tooltip={t('alerting.string-array-input.tooltip-delete', 'delete')}
|
||||
onClick={() => deleteItem(index)}
|
||||
/>
|
||||
)}
|
||||
@@ -55,7 +56,7 @@ export const StringArrayInput = ({ value, onChange, readOnly = false }: Props) =
|
||||
size="sm"
|
||||
onClick={() => onChange([...(value ?? []), ''])}
|
||||
>
|
||||
Add
|
||||
<Trans i18nKey="alerting.string-array-input.add">Add</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { DeepMap, FieldError, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { useControlledFieldArray } from 'app/features/alerting/unified/hooks/useControlledFieldArray';
|
||||
import { NotificationChannelOption } from 'app/types';
|
||||
|
||||
@@ -38,7 +39,7 @@ export const SubformArrayField = ({ option, pathPrefix, errors, defaultValues, r
|
||||
<ActionIcon
|
||||
data-testid={`${path}.${itemIndex}.delete-button`}
|
||||
icon="trash-alt"
|
||||
tooltip="delete"
|
||||
tooltip={t('alerting.subform-array-field.tooltip-delete', 'delete')}
|
||||
onClick={() => remove(itemIndex)}
|
||||
className={styles.deleteIcon}
|
||||
/>
|
||||
@@ -66,7 +67,7 @@ export const SubformArrayField = ({ option, pathPrefix, errors, defaultValues, r
|
||||
size="sm"
|
||||
onClick={() => append({ __id: String(Math.random()) })}
|
||||
>
|
||||
Add
|
||||
<Trans i18nKey="alerting.subform-array-field.add">Add</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { DeepMap, FieldError, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
|
||||
|
||||
import { ActionIcon } from '../../../rules/ActionIcon';
|
||||
@@ -46,7 +47,7 @@ export const SubformField = ({
|
||||
<ActionIcon
|
||||
data-testid={`${name}.delete-button`}
|
||||
icon="trash-alt"
|
||||
tooltip="delete"
|
||||
tooltip={t('alerting.subform-field.tooltip-delete', 'delete')}
|
||||
onClick={() => setShow(false)}
|
||||
className={styles.deleteIcon}
|
||||
/>
|
||||
@@ -78,7 +79,7 @@ export const SubformField = ({
|
||||
onClick={() => setShow(true)}
|
||||
data-testid={`${name}.add-button`}
|
||||
>
|
||||
Add
|
||||
<Trans i18nKey="alerting.subform-field.add">Add</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+11
-2
@@ -4,6 +4,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Box, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
@@ -40,7 +41,9 @@ export function TemplateContentAndPreview({
|
||||
return (
|
||||
<div className={cx(className, styles.mainContainer)}>
|
||||
<div className={styles.container}>
|
||||
<EditorColumnHeader label="Template content" />
|
||||
<EditorColumnHeader
|
||||
label={t('alerting.template-content-and-preview.label-template-content', 'Template content')}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<div className={styles.viewerContainer({ height: 400 })}>
|
||||
<AutoSizer>
|
||||
@@ -60,7 +63,13 @@ export function TemplateContentAndPreview({
|
||||
|
||||
{isGrafanaAlertManager && (
|
||||
<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}>
|
||||
<div
|
||||
role="presentation"
|
||||
|
||||
+20
-6
@@ -16,7 +16,7 @@ import {
|
||||
TextArea,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import {
|
||||
trackEditInputWithTemplate,
|
||||
trackUseCustomInputInTemplate,
|
||||
@@ -173,11 +173,19 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
|
||||
}, [options, valueInForm]);
|
||||
|
||||
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) {
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<div>
|
||||
<Trans i18nKey="alerting.template-selector.loading">Loading...</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -195,8 +203,14 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Select<Template>
|
||||
data-testid="existing-templates-selector"
|
||||
placeholder="Choose notification template"
|
||||
aria-label="Choose notification template"
|
||||
placeholder={t(
|
||||
'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>, _) => {
|
||||
setTemplate(value);
|
||||
}}
|
||||
@@ -272,7 +286,7 @@ function OptionCustomfield({
|
||||
</Label>
|
||||
<TextArea
|
||||
id={id}
|
||||
label="Custom template"
|
||||
label={t('alerting.option-customfield.label-custom-template', 'Custom template')}
|
||||
placeholder={option.placeholder}
|
||||
onChange={(e) => onCustomTemplateChange(e.currentTarget.value)}
|
||||
defaultValue={initialValue}
|
||||
|
||||
@@ -53,7 +53,11 @@ export const AlertRuleNameAndMetric = () => {
|
||||
}
|
||||
>
|
||||
<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
|
||||
data-testid={selectors.components.AlertRules.ruleNameField}
|
||||
id="name"
|
||||
@@ -64,12 +68,16 @@ export const AlertRuleNameAndMetric = () => {
|
||||
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
|
||||
: undefined,
|
||||
})}
|
||||
aria-label="name"
|
||||
aria-label={t('alerting.alert-rule-name-and-metric.aria-label-name', 'name')}
|
||||
placeholder={`Give your ${namePlaceholder} a name`}
|
||||
/>
|
||||
</Field>
|
||||
{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
|
||||
id="metric"
|
||||
width={38}
|
||||
@@ -77,7 +85,7 @@ export const AlertRuleNameAndMetric = () => {
|
||||
required: { value: true, message: 'Must enter a metric name' },
|
||||
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`}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -122,7 +122,7 @@ const AnnotationsStep = () => {
|
||||
)}
|
||||
</>
|
||||
}
|
||||
title="Annotations"
|
||||
title={t('alerting.annotations-step.get-annotations-section-description.title-annotations', 'Annotations')}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -190,7 +190,7 @@ const AnnotationsStep = () => {
|
||||
<Button
|
||||
type="button"
|
||||
className={styles.deleteAnnotationButton}
|
||||
aria-label="delete annotation"
|
||||
aria-label={t('alerting.annotations-step.aria-label-delete-annotation', 'delete annotation')}
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
@@ -212,11 +212,11 @@ const AnnotationsStep = () => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add custom annotation
|
||||
<Trans i18nKey="alerting.annotations-step.add-custom-annotation">Add custom annotation</Trans>
|
||||
</Button>
|
||||
{!selectedDashboard && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, TagList, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { labelsToTags } from '../../utils/labels';
|
||||
import { AlertStateTag } from '../rules/AlertStateTag';
|
||||
@@ -19,14 +20,22 @@ export function CloudAlertPreview({ preview }: CloudAlertPreviewProps) {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<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>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<th>Labels</th>
|
||||
<th>Info</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.cloud-alert-preview.state">State</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.cloud-alert-preview.labels">Labels</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="alerting.cloud-alert-preview.info">Info</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
+6
-2
@@ -3,6 +3,7 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, Input, Select, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { timeOptions } from '../../utils/time';
|
||||
@@ -24,9 +25,12 @@ export const CloudEvaluationBehavior = () => {
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
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
|
||||
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.'
|
||||
>
|
||||
<div className={styles.flexRow}>
|
||||
|
||||
+10
-2
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Input, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
interface CustomAnnotationHeaderFieldProps {
|
||||
field: { onChange: () => void; onBlur: () => void; value: string; name: string };
|
||||
@@ -12,9 +13,16 @@ const CustomAnnotationHeaderField = ({ field }: CustomAnnotationHeaderFieldProps
|
||||
|
||||
return (
|
||||
<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
|
||||
placeholder="Enter custom annotation name..."
|
||||
placeholder={t(
|
||||
'alerting.custom-annotation-header-field.placeholder-enter-custom-annotation-name',
|
||||
'Enter custom annotation name...'
|
||||
)}
|
||||
width={18}
|
||||
{...field}
|
||||
className={styles.customAnnotationInput}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
clearButtonStyles,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
|
||||
import { dashboardApi } from '../../api/dashboardApi';
|
||||
@@ -168,7 +168,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select dashboard and panel"
|
||||
title={t('alerting.dashboard-picker.title-select-dashboard-and-panel', 'Select dashboard and panel')}
|
||||
closeOnEscape
|
||||
isOpen={isOpen}
|
||||
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 */}
|
||||
{!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>
|
||||
Dashboard: {dashboardModel.title} ({dashboardModel.uid}) in folder{' '}
|
||||
{dashboardModel.meta?.folderTitle ?? 'Dashboards'}
|
||||
@@ -193,15 +199,23 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
<FilterInput
|
||||
value={dashboardFilter}
|
||||
onChange={setDashboardFilter}
|
||||
title="Search dashboard"
|
||||
placeholder="Search dashboard"
|
||||
title={t('alerting.dashboard-picker.title-search-dashboard', 'Search dashboard')}
|
||||
placeholder={t('alerting.dashboard-picker.placeholder-search-dashboard', 'Search dashboard')}
|
||||
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}>
|
||||
{isDashSearchFetching && (
|
||||
<LoadingPlaceholder text="Loading dashboards..." className={styles.loadingPlaceholder} />
|
||||
<LoadingPlaceholder
|
||||
text={t('alerting.dashboard-picker.text-loading-dashboards', 'Loading dashboards...')}
|
||||
className={styles.loadingPlaceholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDashSearchFetching && (
|
||||
@@ -224,11 +238,18 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
<div className={styles.column}>
|
||||
{!selectedDashboardUid && !isDashboardFetching && (
|
||||
<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>
|
||||
)}
|
||||
{isDashboardFetching && (
|
||||
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} />
|
||||
<LoadingPlaceholder
|
||||
text={t('alerting.dashboard-picker.text-loading-dashboard', 'Loading dashboard...')}
|
||||
className={styles.loadingPlaceholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedDashboardUid && !isDashboardFetching && (
|
||||
@@ -256,7 +277,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
<Trans i18nKey="alerting.dashboard-picker.confirm">Confirm</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PromQuery } from '@grafana/prometheus';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Alert, Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
||||
|
||||
import { CloudAlertPreview } from './CloudAlertPreview';
|
||||
@@ -91,11 +92,17 @@ export const ExpressionEditor = ({
|
||||
onClick={onRunQueriesClick}
|
||||
disabled={alertPreview?.data.state === LoadingState.Loading}
|
||||
>
|
||||
Preview alerts
|
||||
<Trans i18nKey="alerting.expression-editor.preview-alerts">Preview alerts</Trans>
|
||||
</Button>
|
||||
{previewLoaded && !previewHasAlerts && (
|
||||
<Alert title="Alerts preview" severity="info" className={styles.previewAlert}>
|
||||
There are no firing alerts for your query.
|
||||
<Alert
|
||||
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>
|
||||
)}
|
||||
{previewHasAlerts && <CloudAlertPreview preview={previewDataFrame} />}
|
||||
|
||||
+21
-7
@@ -199,7 +199,7 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
// TODO remove "and alert condition" for recording rules
|
||||
<RuleEditorSection
|
||||
stepNo={step}
|
||||
title="Set evaluation behavior"
|
||||
title={t('alerting.grafana-evaluation-behavior-step.title-set-evaluation-behavior', 'Set evaluation behavior')}
|
||||
description={getDescription(isGrafanaRecordingRule)}
|
||||
>
|
||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||
@@ -252,7 +252,9 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
</Field>
|
||||
</div>
|
||||
<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
|
||||
onClick={onOpenEvaluationGroupCreationModal}
|
||||
type="button"
|
||||
@@ -322,7 +324,10 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
<CollapseToggle
|
||||
isCollapsed={!showErrorHandling}
|
||||
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 && (
|
||||
<>
|
||||
@@ -451,7 +456,7 @@ function EvaluationGroupCreationModal({
|
||||
className={styles.formInput}
|
||||
autoFocus={true}
|
||||
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.' } })}
|
||||
/>
|
||||
</Field>
|
||||
@@ -459,7 +464,13 @@ function EvaluationGroupCreationModal({
|
||||
<Field
|
||||
error={formState.errors.evaluateEvery?.message}
|
||||
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>
|
||||
</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."
|
||||
externalLink={docsLink}
|
||||
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>
|
||||
);
|
||||
@@ -601,7 +615,7 @@ function getDescription(isGrafanaRecordingRule: boolean) {
|
||||
}
|
||||
externalLink={docsLink}
|
||||
linkText={`Read about evaluation and alert states`}
|
||||
title="Alert rule evaluation"
|
||||
title={t('alerting.get-description.title-alert-rule-evaluation', 'Alert rule evaluation')}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+5
-1
@@ -57,7 +57,11 @@ export function GrafanaFolderAndLabelsStep() {
|
||||
}
|
||||
|
||||
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">
|
||||
<FolderSelector />
|
||||
<LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} />
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Field, VirtualizedSelect, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
@@ -44,7 +45,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
|
||||
<div className={style.flexRow}>
|
||||
<Field
|
||||
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,
|
||||
// so avoiding us adding translations for the sake of it
|
||||
// eslint-disable-next-line @grafana/no-untranslated-strings
|
||||
@@ -77,7 +78,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
|
||||
</Field>
|
||||
<Field
|
||||
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,
|
||||
// so avoiding us adding translations for the sake of it
|
||||
// eslint-disable-next-line @grafana/no-untranslated-strings
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Stack, Text, Toggletip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
interface NeedHelpInfoProps {
|
||||
contentText: string | JSX.Element;
|
||||
@@ -39,7 +40,7 @@ export function NeedHelpInfo({ contentText, externalLink, linkText, title = 'Nee
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Icon name="question-circle" size="sm" />
|
||||
<Text variant="bodySmall" color="primary">
|
||||
Need help?
|
||||
<Trans i18nKey="alerting.need-help-info.need-help">Need help?</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFormContext } from 'react-hook-form';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
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 { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
@@ -91,7 +92,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
{isRecordingRuleByType(type) ? (
|
||||
<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>
|
||||
) : (
|
||||
shouldAllowSimplifiedRouting && (
|
||||
@@ -118,7 +121,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
)}
|
||||
{shouldAllowSimplifiedRouting && (
|
||||
<div className={styles.configureNotifications}>
|
||||
<Text element="h5">Recipient</Text>
|
||||
<Text element="h5">
|
||||
<Trans i18nKey="alerting.notifications-step.recipient">Recipient</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{shouldAllowSimplifiedRouting ? ( // when simplified routing is enabled and is grafana rule
|
||||
@@ -258,7 +263,7 @@ function NeedHelpInfoForNotificationPolicy() {
|
||||
</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/"
|
||||
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 { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { previewAlertRule } from '../../api/preview';
|
||||
import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus';
|
||||
@@ -37,11 +38,14 @@ export function PreviewRule(): React.ReactElement | null {
|
||||
<Stack>
|
||||
{allDataSourcesAvailable && (
|
||||
<Button disabled={!isPreviewAvailable} type="button" variant="primary" onClick={onPreview}>
|
||||
Preview alerts
|
||||
<Trans i18nKey="alerting.preview-rule.preview-alerts">Preview alerts</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{!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.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FieldConfigSource, FieldMatcherID, GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { PanelRenderer } from '@grafana/runtime';
|
||||
import { TableCellDisplayMode, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { PreviewRuleResponse } from '../../types/preview';
|
||||
import { RuleFormType } from '../../types/rule-form';
|
||||
@@ -36,7 +37,9 @@ export function PreviewRuleResult(props: Props): React.ReactElement | null {
|
||||
if (data.state === LoadingState.Loading) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span>Loading preview...</span>
|
||||
<span>
|
||||
<Trans i18nKey="alerting.preview-rule-result.loading-preview">Loading preview...</Trans>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, RelativeTimeRange, dateTime, getDefaultRelativeTimeRange, rangeUtil } from '@grafana/data';
|
||||
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 { AlertQueryOptions, MaxDataPointsOption, MinIntervalOption } from './QueryWrapper';
|
||||
@@ -34,7 +35,7 @@ export const QueryOptions = ({
|
||||
content={
|
||||
<div className={styles.queryOptions}>
|
||||
{onChangeTimeRange && (
|
||||
<InlineField label="Time Range">
|
||||
<InlineField label={t('alerting.query-options.label-time-range', 'Time Range')}>
|
||||
<RelativeTimeRangePicker
|
||||
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
|
||||
onChange={(range) => onChangeTimeRange(range, index)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Button, Card, Icon, Stack } from '@grafana/ui';
|
||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
|
||||
@@ -283,7 +284,11 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
|
||||
<EmptyQueryWrapper>
|
||||
<QueryOperationRow title={refId} draggable index={index} id={refId} isOpen collapsable={false}>
|
||||
<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>
|
||||
The datasource for this query was not found, it was either removed or is not installed correctly.
|
||||
</Card.Description>
|
||||
@@ -292,10 +297,10 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="update" variant="secondary" onClick={handleUpdateDatasource}>
|
||||
Update datasource
|
||||
<Trans i18nKey="alerting.datasource-not-found.update-datasource">Update datasource</Trans>
|
||||
</Button>
|
||||
<Button key="remove" variant="destructive" onClick={onRemoveQuery}>
|
||||
Remove query
|
||||
<Trans i18nKey="alerting.datasource-not-found.remove-query">Remove query</Trans>
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
@@ -306,7 +311,7 @@ const DatasourceNotFound = ({ index, onUpdateDatasource, onRemoveQuery, model }:
|
||||
fill="text"
|
||||
size="sm"
|
||||
>
|
||||
Show details
|
||||
<Trans i18nKey="alerting.datasource-not-found.show-details">Show details</Trans>
|
||||
</Button>
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
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 { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
|
||||
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
|
||||
@@ -254,7 +255,7 @@ export function MaxDataPointsOption({
|
||||
return (
|
||||
<InlineField
|
||||
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."
|
||||
>
|
||||
<Input
|
||||
@@ -290,7 +291,7 @@ export function MinIntervalOption({
|
||||
|
||||
return (
|
||||
<InlineField
|
||||
label="Interval"
|
||||
label={t('alerting.min-interval-option.label-interval', 'Interval')}
|
||||
labelWidth={24}
|
||||
tooltip={
|
||||
<>
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||
@@ -18,7 +20,10 @@ export function RecordingRulesNameSpaceAndGroupStep() {
|
||||
<RuleEditorSection
|
||||
stepNo={3}
|
||||
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} />
|
||||
</RuleEditorSection>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ReactElement } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { FieldSet, InlineSwitch, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
export interface RuleEditorSectionProps {
|
||||
title: string;
|
||||
@@ -44,7 +45,7 @@ export const RuleEditorSection = ({
|
||||
onChange={(event) => {
|
||||
switchMode.setAdvancedMode(event.currentTarget.checked);
|
||||
}}
|
||||
label="Advanced options"
|
||||
label={t('alerting.rule-editor-section.label-advanced-options', 'Advanced options')}
|
||||
showLabel
|
||||
transparent
|
||||
className={styles.reverse}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user