I18n: Update eslint rule to catch some untranslated object properties (#105072)
This commit is contained in:
+8
-2
@@ -300,9 +300,15 @@ module.exports = [
|
||||
'packages/grafana-ui/**/*.{ts,tsx,js,jsx}',
|
||||
...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{ts,tsx,js,jsx}`),
|
||||
],
|
||||
ignores: ['**/*.story.tsx', '**/*.{test,spec}.{ts,tsx}', '**/__mocks__/', 'public/test', '**/spec/**/*.{ts,tsx}'],
|
||||
ignores: [
|
||||
'public/test/**',
|
||||
'**/*.{test,spec,story}.{ts,tsx}',
|
||||
'**/{tests,__mocks__,__tests__,fixtures,spec,mocks}/**',
|
||||
'**/{test-utils,testHelpers,mocks}.{ts,tsx}',
|
||||
'**/mock*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'@grafana/no-untranslated-strings': 'error',
|
||||
'@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
|
||||
'@grafana/no-translation-top-level': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ module.exports = {
|
||||
moduleDirectories: ['public', 'node_modules'],
|
||||
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts/tests'],
|
||||
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'cjs'],
|
||||
setupFiles: ['jest-canvas-mock', './public/test/jest-setup.ts'],
|
||||
testTimeout: 30000,
|
||||
resolver: `<rootDir>/public/test/jest-resolver.js`,
|
||||
|
||||
@@ -114,7 +114,47 @@ Used to find all instances of `theme` tokens being used in the codebase and emit
|
||||
|
||||
### `no-untranslated-strings`
|
||||
|
||||
Check if strings are marked for translation.
|
||||
Check if strings are marked for translation inside JSX Elements, in certain JSX props, and in certain object properties.
|
||||
|
||||
### Options
|
||||
|
||||
#### `forceFix`
|
||||
|
||||
Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves.
|
||||
|
||||
#### Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
'@grafana/no-untranslated-strings': ['error', { forceFix: ['app/features/some-feature'] }],
|
||||
}
|
||||
```
|
||||
|
||||
#### `calleesToIgnore`
|
||||
|
||||
Allows specifying regexes for methods that should be ignored when checking if object properties are untranslated.
|
||||
|
||||
This is particularly useful to exclude references to properties such as `label` inside `css()` calls.
|
||||
|
||||
#### Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
'@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$'] }],
|
||||
}
|
||||
|
||||
// The below would not be reported as an error
|
||||
const foo = css({
|
||||
label: 'test',
|
||||
});
|
||||
|
||||
// The below would still be reported as an error
|
||||
const bar = {
|
||||
label: 'test',
|
||||
};
|
||||
```
|
||||
|
||||
#### JSXText
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
@@ -126,7 +166,30 @@ Check if strings are marked for translation.
|
||||
<InlineToast placement="top" referenceElement={buttonRef.current}>
|
||||
<Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
|
||||
</InlineToast>
|
||||
```
|
||||
|
||||
#### JSXAttributes
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
<div title="foo bar" />
|
||||
|
||||
// Good ✅
|
||||
<div title={t('some.key.foo-bar', 'foo bar')} />
|
||||
```
|
||||
|
||||
#### Object properties
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
const someConfig = {
|
||||
label: 'Some label',
|
||||
};
|
||||
|
||||
// Good ✅
|
||||
const getSomeConfig = () => ({
|
||||
label: t('some.key.label', 'Some label'),
|
||||
});
|
||||
```
|
||||
|
||||
#### Passing variables to translations
|
||||
@@ -166,8 +229,11 @@ The below props are checked for untranslated strings:
|
||||
- `placeholder`
|
||||
- `aria-label`
|
||||
- `title`
|
||||
- `subTitle`
|
||||
- `text`
|
||||
- `tooltip`
|
||||
- `message`
|
||||
- `name`
|
||||
|
||||
```tsx
|
||||
// Bad ❌
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{ forceFix: string[] }]>} RuleDefinition */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[] }]>} RuleDefinition */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[]}]>} RuleContextWithOptions */
|
||||
|
||||
const {
|
||||
getNodeValue,
|
||||
@@ -20,13 +21,107 @@ const createRule = ESLintUtils.RuleCreator(
|
||||
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`
|
||||
);
|
||||
|
||||
/** @type {string[]} */
|
||||
/**
|
||||
* JSX props to check for untranslated strings
|
||||
*/
|
||||
const propsToCheck = ['content', 'label', 'description', 'placeholder', 'aria-label', 'title', 'text', 'tooltip'];
|
||||
|
||||
/**
|
||||
* Object properties to check for untranslated strings
|
||||
*/
|
||||
const propertiesToCheck = [
|
||||
'label',
|
||||
'description',
|
||||
'placeholder',
|
||||
'aria-label',
|
||||
'title',
|
||||
'subTitle',
|
||||
'text',
|
||||
'tooltip',
|
||||
'message',
|
||||
];
|
||||
|
||||
/** @type {RuleDefinition} */
|
||||
const noUntranslatedStrings = createRule({
|
||||
/**
|
||||
* @param {RuleContextWithOptions} context
|
||||
*/
|
||||
create(context) {
|
||||
const calleesToIgnore = context.options[0]?.calleesToIgnore || [];
|
||||
const propertiesRegexes = calleesToIgnore.map((pattern) => {
|
||||
return new RegExp(pattern);
|
||||
});
|
||||
|
||||
return {
|
||||
Property(node) {
|
||||
const { key, value, parent, computed } = node;
|
||||
const keyName = (() => {
|
||||
if (computed) {
|
||||
return null;
|
||||
}
|
||||
if (key.type === AST_NODE_TYPES.Identifier && typeof key.name === 'string') {
|
||||
return key.name;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
// Catch cases of default props setting object properties, which would be at the top level
|
||||
const isAssignmentPattern = parent.parent.type === AST_NODE_TYPES.AssignmentPattern;
|
||||
|
||||
if (
|
||||
!keyName ||
|
||||
!propertiesToCheck.includes(keyName) ||
|
||||
parent.type === AST_NODE_TYPES.ObjectPattern ||
|
||||
isAssignmentPattern
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callExpression = parent.parent.type === AST_NODE_TYPES.CallExpression ? parent.parent.callee : null;
|
||||
// Check if we're being called by something that we want to ignore
|
||||
// e.g. css({ label: 'test' }) should be ignored (based on the rule configuration)
|
||||
if (
|
||||
callExpression?.type === AST_NODE_TYPES.Identifier &&
|
||||
propertiesRegexes.some((regex) => regex.test(callExpression.name))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeValue = getNodeValue(node);
|
||||
|
||||
const isOnlySymbols = !/[a-zA-Z0-9]/.test(nodeValue);
|
||||
const isNumeric = !/[a-zA-Z]/.test(nodeValue);
|
||||
|
||||
const isUntranslated =
|
||||
((value.type === AST_NODE_TYPES.Literal && nodeValue !== '') ||
|
||||
value.type === AST_NODE_TYPES.TemplateLiteral) &&
|
||||
!isOnlySymbols &&
|
||||
!isNumeric;
|
||||
|
||||
const errorCanBeFixed = canBeFixed(node, context);
|
||||
const errorShouldBeFixed = shouldBeFixed(context);
|
||||
if (
|
||||
isUntranslated &&
|
||||
// TODO: Remove this check in the future when we've fixed all cases of untranslated properties
|
||||
// For now, we're only reporting the issues that can be auto-fixed, rather than adding to betterer results
|
||||
errorCanBeFixed
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noUntranslatedStringsProperties',
|
||||
fix: errorCanBeFixed && errorShouldBeFixed ? getTFixers(node, context) : undefined,
|
||||
suggest: errorCanBeFixed
|
||||
? [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
fix: getTFixers(node, context),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
if (!propsToCheck.includes(String(node.name.name)) || !node.value) {
|
||||
return;
|
||||
@@ -163,6 +258,7 @@ const noUntranslatedStrings = createRule({
|
||||
messages: {
|
||||
noUntranslatedStrings: 'No untranslated strings. Wrap text with <Trans />',
|
||||
noUntranslatedStringsProp: `No untranslated strings in text props. Wrap text with <Trans /> or use t()`,
|
||||
noUntranslatedStringsProperties: `No untranslated strings in object properties. Wrap text with t()`,
|
||||
wrapWithTrans: 'Wrap text with <Trans /> for manual key assignment',
|
||||
wrapWithT: 'Wrap text with t() for manual key assignment',
|
||||
},
|
||||
@@ -177,12 +273,19 @@ const noUntranslatedStrings = createRule({
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
calleesToIgnore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultOptions: [{ forceFix: [] }],
|
||||
defaultOptions: [{ forceFix: [], calleesToIgnore: [] }],
|
||||
});
|
||||
|
||||
module.exports = noUntranslatedStrings;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXText} JSXText */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[]}]>} RuleContextWithOptions */
|
||||
|
||||
const { AST_NODE_TYPES } = require('@typescript-eslint/utils');
|
||||
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ function shouldBeFixed(context) {
|
||||
|
||||
/**
|
||||
* Checks if a node can be fixed automatically
|
||||
* @param {JSXAttribute|JSXElement|JSXFragment} node The node to check
|
||||
* @param {JSXAttribute|JSXElement|JSXFragment|Property} node The node to check
|
||||
* @param {RuleContextWithOptions} context
|
||||
* @returns {boolean} Whether the node can be fixed
|
||||
*/
|
||||
@@ -86,16 +86,28 @@ function canBeFixed(node, context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentMethod = getParentMethod(node, context);
|
||||
const isAttribute = node.type === AST_NODE_TYPES.JSXAttribute;
|
||||
const isProperty = node.type === AST_NODE_TYPES.Property;
|
||||
const isPropertyOrAttribute = isAttribute || isProperty;
|
||||
|
||||
// We can only fix JSX attribute strings that are within a function,
|
||||
// otherwise the `t` function call will be made too early
|
||||
if (node.type === AST_NODE_TYPES.JSXAttribute) {
|
||||
const parentMethod = getParentMethod(node, context);
|
||||
if (!parentMethod) {
|
||||
if (isPropertyOrAttribute && !parentMethod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're going to try and fix using `t`, and it already exists in the scope,
|
||||
// but not from `useTranslate`, then we can't fix/provide a suggestion
|
||||
if (isPropertyOrAttribute && parentMethod) {
|
||||
const hasTDeclaration = getTDeclaration(parentMethod, context);
|
||||
const hasUseTranslateDeclaration = methodHasUseTranslate(parentMethod, context);
|
||||
if (hasTDeclaration && !hasUseTranslateDeclaration) {
|
||||
return false;
|
||||
}
|
||||
if (node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
||||
return isStringLiteral(node.value.expression);
|
||||
}
|
||||
}
|
||||
if (isAttribute && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
||||
return isStringLiteral(node.value.expression);
|
||||
}
|
||||
|
||||
const values =
|
||||
@@ -130,7 +142,7 @@ function canBeFixed(node, context) {
|
||||
*/
|
||||
function getTranslationPrefix(context) {
|
||||
const filename = context.filename;
|
||||
const match = filename.match(/public\/app\/features\/([^/]+)/);
|
||||
const match = filename.match(/public\/app\/features\/(.+?)\//);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
@@ -139,7 +151,7 @@ function getTranslationPrefix(context) {
|
||||
|
||||
/**
|
||||
* Gets the i18n key for a node
|
||||
* @param {JSXAttribute|JSXText} node The node
|
||||
* @param {JSXAttribute|JSXText|Property} node The node
|
||||
* @param {RuleContextWithOptions} context
|
||||
* @returns {string} The i18n key
|
||||
*/
|
||||
@@ -148,6 +160,10 @@ const getI18nKey = (node, context) => {
|
||||
const stringValue = getNodeValue(node);
|
||||
|
||||
const componentNames = getComponentNames(node, context);
|
||||
|
||||
const propertyName =
|
||||
node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier ? String(node.key.name) : null;
|
||||
|
||||
const words = stringValue
|
||||
.trim()
|
||||
.replace(/[^\a-zA-Z\s]/g, '')
|
||||
@@ -185,14 +201,14 @@ const getI18nKey = (node, context) => {
|
||||
kebabString = [potentialId, propName, kebabString].filter(Boolean).join('-');
|
||||
}
|
||||
|
||||
const fullPrefix = [prefixFromFilePath, ...componentNames, kebabString].filter(Boolean).join('.');
|
||||
const fullPrefix = [prefixFromFilePath, ...componentNames, propertyName, kebabString].filter(Boolean).join('.');
|
||||
|
||||
return fullPrefix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets component names from ancestors
|
||||
* @param {JSXAttribute|JSXText} node The node
|
||||
* @param {JSXAttribute|JSXText|Property} node The node
|
||||
* @param {RuleContextWithOptions} context
|
||||
* @returns {string[]} The component names
|
||||
*/
|
||||
@@ -218,13 +234,22 @@ function getComponentNames(node, context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a method has a variable declaration of `t`
|
||||
* that came from a `useTranslate` call
|
||||
* @param {Node} method The node
|
||||
* For a given node, check the scope and find a variable declaration of `t`
|
||||
* @param {Node} node
|
||||
* @param {RuleContextWithOptions} context
|
||||
*/
|
||||
function methodHasUseTranslate(method, context) {
|
||||
const tDeclaration = method ? context.sourceCode.getScope(method).variables.find((v) => v.name === 't') : null;
|
||||
function getTDeclaration(node, context) {
|
||||
return context.sourceCode.getScope(node).variables.find((v) => v.name === 't');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node has a variable declaration of `t`
|
||||
* that came from a `useTranslate` call
|
||||
* @param {Node} node The node
|
||||
* @param {RuleContextWithOptions} context
|
||||
*/
|
||||
function methodHasUseTranslate(node, context) {
|
||||
const tDeclaration = getTDeclaration(node, context);
|
||||
return (
|
||||
tDeclaration &&
|
||||
tDeclaration.defs.find((definition) => {
|
||||
@@ -243,7 +268,7 @@ function methodHasUseTranslate(method, context) {
|
||||
|
||||
/**
|
||||
* Gets the import fixer for a node
|
||||
* @param {JSXElement|JSXFragment|JSXAttribute} node
|
||||
* @param {JSXElement|JSXFragment|JSXAttribute|Property} node
|
||||
* @param {RuleFixer} fixer The fixer
|
||||
* @param {'Trans'|'t'|'useTranslate'} importName The member to import from either `@grafana/i18n` or `@grafana/i18n/internal`
|
||||
* @param {RuleContextWithOptions} context
|
||||
@@ -337,7 +362,7 @@ const firstCharIsUpper = (str) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {JSXAttribute} node
|
||||
* @param {JSXAttribute|Property} node
|
||||
* @param {RuleFixer} fixer
|
||||
* @param {RuleContextWithOptions} context
|
||||
* @returns {import('@typescript-eslint/utils/ts-eslint').RuleFix|undefined} The fix
|
||||
@@ -380,9 +405,10 @@ const getUseTranslateFixer = (node, fixer, context) => {
|
||||
if (!returnStatementIsJsx) {
|
||||
return;
|
||||
}
|
||||
const tDeclarationExists = getTDeclaration(parentMethod, context);
|
||||
const useTranslateExists = methodHasUseTranslate(parentMethod, context);
|
||||
|
||||
if (useTranslateExists) {
|
||||
if (tDeclarationExists && useTranslateExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -396,7 +422,7 @@ const getUseTranslateFixer = (node, fixer, context) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {JSXAttribute} node
|
||||
* @param {JSXAttribute|Property} node
|
||||
* @param {RuleContextWithOptions} context
|
||||
* @returns {(fixer: RuleFixer) => import('@typescript-eslint/utils/ts-eslint').RuleFix[]}
|
||||
*/
|
||||
@@ -406,9 +432,13 @@ const getTFixers = (node, context) => (fixer) => {
|
||||
const value = getNodeValue(node);
|
||||
const wrappingQuotes = value.includes('"') ? "'" : '"';
|
||||
|
||||
fixes.push(
|
||||
fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`)
|
||||
);
|
||||
if (node.type === AST_NODE_TYPES.Property) {
|
||||
fixes.push(fixer.replaceText(node.value, `t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})`));
|
||||
} else {
|
||||
fixes.push(
|
||||
fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we need to add `useTranslate` to the node
|
||||
const useTranslateFixer = getUseTranslateFixer(node, fixer, context);
|
||||
@@ -428,11 +458,19 @@ const getTFixers = (node, context) => (fixer) => {
|
||||
|
||||
/**
|
||||
* Gets the value of a node
|
||||
* @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild} node The node
|
||||
* @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild|Property} node The node
|
||||
* @returns {string} The node value
|
||||
*/
|
||||
function getNodeValue(node) {
|
||||
if (node.type === AST_NODE_TYPES.JSXAttribute && node.value?.type === AST_NODE_TYPES.Literal) {
|
||||
if (
|
||||
(node.type === AST_NODE_TYPES.JSXAttribute || node.type === AST_NODE_TYPES.Property) &&
|
||||
node.value?.type === AST_NODE_TYPES.Literal
|
||||
) {
|
||||
// TODO: Update this to return bool/number values and handle the type issues elsewhere
|
||||
// For now, we'll just return an empty string so we consider any numbers or booleans as not being issues
|
||||
if (typeof node.value.value === 'boolean' || typeof node.value.value === 'number') {
|
||||
return '';
|
||||
}
|
||||
return String(node.value.value) || '';
|
||||
}
|
||||
if (node.type === AST_NODE_TYPES.JSXText) {
|
||||
|
||||
@@ -14,7 +14,7 @@ RuleTester.setDefaultConfig({
|
||||
},
|
||||
});
|
||||
|
||||
const filename = 'public/app/features/some-feature/SomeFile.tsx';
|
||||
const filename = 'public/app/features/some-feature/nested/SomeFile.tsx';
|
||||
|
||||
const packageName = '@grafana/i18n';
|
||||
|
||||
@@ -31,6 +31,7 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
|
||||
{
|
||||
name: 'Text in Trans component',
|
||||
code: `const Foo = () => <Bar><Trans>Translated text</Trans></Bar>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Text in Trans component with whitespace/JSXText elements',
|
||||
@@ -38,58 +39,72 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
|
||||
<Trans>
|
||||
Translated text
|
||||
</Trans> </Bar>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Empty component',
|
||||
code: `<div> </div>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Text using t() function',
|
||||
code: `<div>{t('translated.key', 'Translated text')}</div>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Prop using t() function',
|
||||
code: `<div aria-label={t('aria.label', 'Accessible label')} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Empty string prop',
|
||||
code: `<div title="" />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Prop using boolean',
|
||||
code: `<div title={false} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Prop using number',
|
||||
code: `<div title={0} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Prop using null',
|
||||
code: `<div title={null} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Prop using undefined',
|
||||
code: `<div title={undefined} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Variable interpolation',
|
||||
code: `<div>{variable}</div>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Entirely non-alphanumeric text (prop)',
|
||||
code: `<div title="-" />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Entirely non-alphanumeric text',
|
||||
code: `<div>-</div>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Non-alphanumeric siblings',
|
||||
code: `<div>({variable})</div>`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: "Ternary in an attribute we don't care about",
|
||||
code: `<div icon={isAThing ? 'Foo' : 'Bar'} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Ternary with falsy strings',
|
||||
@@ -98,6 +113,76 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
|
||||
{
|
||||
name: 'Ternary with no strings',
|
||||
code: `<div title={isAThing ? 1 : 2} />`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Object property',
|
||||
code: `const getThing = () => ({
|
||||
label: t('test', 'Test'),
|
||||
})`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
// Ideally we would catch this, but test case is to ensure that
|
||||
// we aren't reporting an error
|
||||
name: 'Object property using variable',
|
||||
code: `
|
||||
const getThing = () => {
|
||||
const foo = 'test';
|
||||
const thing = {
|
||||
label: foo,
|
||||
}
|
||||
}`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Object property using dynamic/other keys',
|
||||
code: `
|
||||
const getThing = () => {
|
||||
const foo = 'label';
|
||||
const label = 'not-a-label';
|
||||
const thing = {
|
||||
1: 'a',
|
||||
// We can't easily check for computed keys, so for now don't worry about this case
|
||||
[foo]: 'test',
|
||||
|
||||
// This is dumb, but we need to check that we don't confuse
|
||||
// the name of the variable for the key name
|
||||
[label]: 'test',
|
||||
['title']: 'test',
|
||||
}
|
||||
}`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Label reference inside `css` call',
|
||||
code: `const getThing = () => {
|
||||
const thing = css({
|
||||
label: 'red',
|
||||
});
|
||||
}`,
|
||||
options: [{ calleesToIgnore: ['somethingelse', '^css$'] }],
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Object property value that is a boolean or number',
|
||||
code: `const getThing = () => {
|
||||
const thing = {
|
||||
label: true,
|
||||
title: 1
|
||||
};
|
||||
}`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Object property at top level',
|
||||
code: `const thing = { label: 'test' }`,
|
||||
filename,
|
||||
},
|
||||
{
|
||||
name: 'Object property in default props',
|
||||
code: `const Foo = ({ foobar = {label: 'test'} }) => <div>{foobar.label}</div>`,
|
||||
filename,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
@@ -667,6 +752,97 @@ const Foo = () => {
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Untranslated object property',
|
||||
code: `
|
||||
const Foo = () => {
|
||||
const thing = {
|
||||
label: 'test',
|
||||
}
|
||||
|
||||
return <div>{thing.label}</div>;
|
||||
}`,
|
||||
filename,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStringsProperties',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
output: `
|
||||
${USE_TRANSLATE_IMPORT}
|
||||
const Foo = () => {
|
||||
const { t } = useTranslate();
|
||||
const thing = {
|
||||
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
|
||||
}
|
||||
|
||||
return <div>{thing.label}</div>;
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Untranslated object property with existing import',
|
||||
code: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
const thing = {
|
||||
label: 'test',
|
||||
}
|
||||
}`,
|
||||
filename,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStringsProperties',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
output: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
const thing = {
|
||||
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Untranslated object property with calleesToIgnore',
|
||||
code: `
|
||||
const Foo = () => {
|
||||
const thing = doAThing({
|
||||
label: 'test',
|
||||
})
|
||||
}`,
|
||||
options: [{ calleesToIgnore: ['doSomethingElse'] }],
|
||||
filename,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStringsProperties',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
output: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
const thing = doAThing({
|
||||
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
|
||||
})
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* AUTO FIXES
|
||||
*/
|
||||
@@ -723,6 +899,42 @@ return <div title={t("some-feature.foo.title-foo", "foo")} />
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Auto fixes object property',
|
||||
code: `
|
||||
const Foo = () => {
|
||||
return {
|
||||
label: 'test',
|
||||
}
|
||||
}`,
|
||||
filename,
|
||||
options: [{ forceFix: ['public/app/features/some-feature'] }],
|
||||
output: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
return {
|
||||
label: t("some-feature.foo.label.test", "test"),
|
||||
}
|
||||
}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStringsProperties',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
output: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
return {
|
||||
label: t("some-feature.foo.label.test", "test"),
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* UNFIXABLE CASES
|
||||
*/
|
||||
@@ -824,5 +1036,34 @@ const Foo = () => {
|
||||
filename,
|
||||
errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Cannot fix if `t` already exists from somewhere else',
|
||||
code: `
|
||||
const Foo = () => {
|
||||
const t = () => 'something else';
|
||||
return (
|
||||
<div title="foo" />
|
||||
)
|
||||
}`,
|
||||
filename,
|
||||
errors: [{ messageId: 'noUntranslatedStringsProp' }],
|
||||
},
|
||||
|
||||
// TODO: Enable test once all top-level issues have been fixed
|
||||
// and rule is enabled again
|
||||
// {
|
||||
// name: 'Object property at top level scope',
|
||||
// code: `
|
||||
// const thing = {
|
||||
// label: 'test',
|
||||
// }`,
|
||||
// filename,
|
||||
// errors: [
|
||||
// {
|
||||
// messageId: 'noUntranslatedStringsProperties',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -54,7 +54,13 @@ export const ActionVariablesEditor = ({ value, onChange }: Props) => {
|
||||
|
||||
const isAddButtonDisabled = name === '' || key === '';
|
||||
|
||||
const variableTypeOptions: ComboboxOption[] = [{ label: 'string', value: ActionVariableType.String }];
|
||||
const variableTypeOptions: ComboboxOption[] = [
|
||||
{
|
||||
// eslint-disable-next-line @grafana/no-untranslated-strings
|
||||
label: 'string',
|
||||
value: ActionVariableType.String,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -86,7 +86,10 @@ const AdminEditOrgPage = () => {
|
||||
const pageNav: NavModelItem = {
|
||||
text: orgState?.value?.name ?? '',
|
||||
icon: 'shield',
|
||||
subTitle: 'Manage settings and user roles for an organization.',
|
||||
subTitle: t(
|
||||
'admin.admin-edit-org-page.page-nav.subTitle.manage-settings-roles-organization',
|
||||
'Manage settings and user roles for an organization.'
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { featureEnabled } from '@grafana/runtime';
|
||||
import { Stack } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
@@ -59,6 +60,7 @@ export const UserAdminPage = ({
|
||||
revokeAllSessions,
|
||||
syncLdapUser,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate();
|
||||
const { id = '' } = useParams();
|
||||
useEffect(() => {
|
||||
loadAdminUserPage(id);
|
||||
@@ -123,7 +125,10 @@ export const UserAdminPage = ({
|
||||
const pageNav: NavModelItem = {
|
||||
text: user?.login ?? '',
|
||||
icon: 'shield',
|
||||
subTitle: 'Manage settings for an individual user.',
|
||||
subTitle: t(
|
||||
'admin.user-admin-page.page-nav.subTitle.manage-settings-for-an-individual-user',
|
||||
'Manage settings for an individual user.'
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -88,8 +88,11 @@ const UserListAdminPageUnConnected = ({
|
||||
/>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'All users', value: false },
|
||||
{ label: 'Active last 30 days', value: true },
|
||||
{ label: t('admin.user-list-admin-page-un-connected.label.all-users', 'All users'), value: false },
|
||||
{
|
||||
label: t('admin.user-list-admin-page-un-connected.label.active-last-days', 'Active last 30 days'),
|
||||
value: true,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => changeFilter({ name: 'activeLast30Days', value })}
|
||||
value={filters.find((f) => f.name === 'activeLast30Days')?.value}
|
||||
|
||||
@@ -70,7 +70,15 @@ const UserListAnonymousDevicesPageUnConnected = ({
|
||||
onChange={changeAnonQuery}
|
||||
/>
|
||||
<RadioButtonGroup
|
||||
options={[{ label: 'Active last 30 days', value: true }]}
|
||||
options={[
|
||||
{
|
||||
label: t(
|
||||
'admin.user-list-anonymous-devices-page-un-connected.label.active-last-days',
|
||||
'Active last 30 days'
|
||||
),
|
||||
value: true,
|
||||
},
|
||||
]}
|
||||
// onChange={(value) => changeFilter({ name: 'activeLast30Days', value })}
|
||||
value={filters.find((f) => f.name === 'activeLast30Days')?.value}
|
||||
className={styles.filter}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isArray, reduce } from 'lodash';
|
||||
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { IconName } from '@grafana/ui';
|
||||
import { QueryPart, QueryPartDef } from 'app/features/alerting/state/query_part';
|
||||
|
||||
@@ -114,42 +115,42 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
|
||||
case 'normal':
|
||||
case 'ok': {
|
||||
return {
|
||||
text: 'OK',
|
||||
text: t('alerting.get-state-display-model.text.ok', 'OK'),
|
||||
iconClass: 'heart',
|
||||
stateClass: 'alert-state-ok',
|
||||
};
|
||||
}
|
||||
case 'alerting': {
|
||||
return {
|
||||
text: 'ALERTING',
|
||||
text: t('alerting.get-state-display-model.text.alerting', 'ALERTING'),
|
||||
iconClass: 'heart-break',
|
||||
stateClass: 'alert-state-critical',
|
||||
};
|
||||
}
|
||||
case 'nodata': {
|
||||
return {
|
||||
text: 'NO DATA',
|
||||
text: t('alerting.get-state-display-model.text.no-data', 'NO DATA'),
|
||||
iconClass: 'question-circle',
|
||||
stateClass: 'alert-state-warning',
|
||||
};
|
||||
}
|
||||
case 'paused': {
|
||||
return {
|
||||
text: 'PAUSED',
|
||||
text: t('alerting.get-state-display-model.text.paused', 'PAUSED'),
|
||||
iconClass: 'pause',
|
||||
stateClass: 'alert-state-paused',
|
||||
};
|
||||
}
|
||||
case 'pending': {
|
||||
return {
|
||||
text: 'PENDING',
|
||||
text: t('alerting.get-state-display-model.text.pending', 'PENDING'),
|
||||
iconClass: 'hourglass',
|
||||
stateClass: 'alert-state-warning',
|
||||
};
|
||||
}
|
||||
case 'recovering': {
|
||||
return {
|
||||
text: 'RECOVERING',
|
||||
text: t('alerting.get-state-display-model.text.recovering', 'RECOVERING'),
|
||||
iconClass: 'hourglass',
|
||||
stateClass: 'alert-state-warning',
|
||||
};
|
||||
@@ -157,7 +158,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
|
||||
|
||||
case 'firing': {
|
||||
return {
|
||||
text: 'FIRING',
|
||||
text: t('alerting.get-state-display-model.text.firing', 'FIRING'),
|
||||
iconClass: 'fire',
|
||||
stateClass: '',
|
||||
};
|
||||
@@ -165,7 +166,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
|
||||
|
||||
case 'inactive': {
|
||||
return {
|
||||
text: 'INACTIVE',
|
||||
text: t('alerting.get-state-display-model.text.inactive', 'INACTIVE'),
|
||||
iconClass: 'check',
|
||||
stateClass: '',
|
||||
};
|
||||
@@ -173,7 +174,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
|
||||
|
||||
case 'error': {
|
||||
return {
|
||||
text: 'ERROR',
|
||||
text: t('alerting.get-state-display-model.text.error', 'ERROR'),
|
||||
iconClass: 'heart-break',
|
||||
stateClass: 'alert-state-critical',
|
||||
};
|
||||
@@ -182,7 +183,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
|
||||
case 'unknown':
|
||||
default: {
|
||||
return {
|
||||
text: 'UNKNOWN',
|
||||
text: t('alerting.get-state-display-model.text.unknown', 'UNKNOWN'),
|
||||
iconClass: 'question-circle',
|
||||
stateClass: '.alert-state-paused',
|
||||
};
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
|
||||
function FeatureTogglePage() {
|
||||
const { t } = useTranslate();
|
||||
const navModel: NavModel = {
|
||||
node: {
|
||||
text: 'Alerting is not enabled',
|
||||
text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'),
|
||||
hideFromBreadcrumbs: true,
|
||||
subTitle: 'To enable alerting, enable it in the Grafana config',
|
||||
subTitle: t(
|
||||
'alerting.feature-toggle-page.nav-model.subTitle.enable-alerting-grafana-config',
|
||||
'To enable alerting, enable it in the Grafana config'
|
||||
),
|
||||
},
|
||||
main: {
|
||||
text: 'Alerting is not enabled',
|
||||
text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import {
|
||||
defaultsFromQuery,
|
||||
getDefaultSilenceFormValues,
|
||||
@@ -37,10 +38,14 @@ const SilencesEditorComponent = () => {
|
||||
};
|
||||
|
||||
function NewSilencePage() {
|
||||
const { t } = useTranslate();
|
||||
const pageNav = {
|
||||
id: 'silence-new',
|
||||
text: 'Silence alert rule',
|
||||
subTitle: 'Configure silences to stop notifications from a particular alert rule',
|
||||
text: t('alerting.new-silence-page.page-nav.text.silence-alert-rule', 'Silence alert rule'),
|
||||
subTitle: t(
|
||||
'alerting.new-silence-page.page-nav.subTitle.configure-silences-notifications-particular-alert',
|
||||
'Configure silences to stop notifications from a particular alert rule'
|
||||
),
|
||||
};
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId="silences" pageNav={pageNav} accessType="instance">
|
||||
|
||||
+5
-4
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
@@ -22,13 +22,14 @@ type AlertGroupAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAler
|
||||
type AlertGroupAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>;
|
||||
|
||||
export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const columns = useMemo(
|
||||
(): AlertGroupAlertsTableColumnProps[] => [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'Notification state',
|
||||
label: t('alerting.alert-group-alerts-table.columns.label.notification-state', 'Notification state'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: alert }) => (
|
||||
<>
|
||||
@@ -52,13 +53,13 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props)
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Instance labels',
|
||||
label: t('alerting.alert-group-alerts-table.columns.label.instance-labels', 'Instance labels'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />,
|
||||
size: 1,
|
||||
},
|
||||
],
|
||||
[styles]
|
||||
[styles, t]
|
||||
);
|
||||
|
||||
const items = useMemo(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
|
||||
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
|
||||
|
||||
function ExportNewGrafanaRulePage() {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
pageNav={{
|
||||
text: 'Export new Grafana rule',
|
||||
text: t('alerting.export-new-grafana-rule-page.text.export-new-grafana-rule', 'Export new Grafana rule'),
|
||||
subTitle: 'Export a new rule definition in Terraform(HCL) format. Any changes you make will not be saved.',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { Alert, Button, ClipboardButton, CodeEditor, TextLink, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ExportFormats, ExportProvider, ProvisioningType, allGrafanaExportProviders } from './providers';
|
||||
@@ -92,11 +92,15 @@ const fileExportPreviewStyles = (theme: GrafanaTheme2) => ({
|
||||
});
|
||||
|
||||
function FileExportInlineDocumentation({ exportProvider }: { exportProvider: ExportProvider<unknown> }) {
|
||||
const { t } = useTranslate();
|
||||
const { name, type } = exportProvider;
|
||||
|
||||
const exportInlineDoc: Record<ProvisioningType, { title: string; component: React.ReactNode }> = {
|
||||
file: {
|
||||
title: 'File-provisioning format',
|
||||
title: t(
|
||||
'alerting.file-export-inline-documentation.export-inline-doc.title.fileprovisioning-format',
|
||||
'File-provisioning format'
|
||||
),
|
||||
component: (
|
||||
<Trans i18nKey="alerting.file-export-inline-documentation.file-provisioning">
|
||||
{{ name }} format is only valid for File Provisioning.{' '}
|
||||
@@ -110,7 +114,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
|
||||
),
|
||||
},
|
||||
api: {
|
||||
title: 'API-provisioning format',
|
||||
title: t(
|
||||
'alerting.file-export-inline-documentation.export-inline-doc.title.apiprovisioning-format',
|
||||
'API-provisioning format'
|
||||
),
|
||||
component: (
|
||||
<Trans i18nKey="alerting.file-export-inline-documentation.api-provisioning">
|
||||
{{ name }} format is only valid for API Provisioning.{' '}
|
||||
@@ -124,7 +131,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
|
||||
),
|
||||
},
|
||||
terraform: {
|
||||
title: 'Terraform-provisioning format',
|
||||
title: t(
|
||||
'alerting.file-export-inline-documentation.export-inline-doc.title.terraformprovisioning-format',
|
||||
'Terraform-provisioning format'
|
||||
),
|
||||
component: (
|
||||
<Trans i18nKey="alerting.file-export-inline-documentation.terraform-provisioning">
|
||||
{{ name }} format is only valid for Terraform Provisioning.{' '}
|
||||
|
||||
@@ -93,11 +93,12 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
|
||||
}
|
||||
|
||||
function GrafanaModifyExportPage() {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
pageNav={{
|
||||
text: 'Modify export',
|
||||
text: t('alerting.grafana-modify-export-page.text.modify-export', 'Modify export'),
|
||||
subTitle:
|
||||
'Modify the current alert rule and export the rule definition in the format of your choice. Any changes you make will not be saved.',
|
||||
}}
|
||||
|
||||
@@ -240,7 +240,13 @@ const ImportFromDSRules = () => {
|
||||
name="targetDatasourceUID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a target data source' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.import-from-dsrules.message.please-select-a-target-data-source',
|
||||
'Please select a target data source'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
|
||||
import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams';
|
||||
|
||||
@@ -39,10 +40,14 @@ const EditTimingRoute = () => {
|
||||
};
|
||||
|
||||
function EditMuteTimingPage() {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<AlertmanagerPageWrapper
|
||||
navId="am-routes"
|
||||
pageNav={{ id: 'alert-policy-edit', text: 'Edit time interval' }}
|
||||
pageNav={{
|
||||
id: 'alert-policy-edit',
|
||||
text: t('alerting.edit-mute-timing-page.text.edit-time-interval', 'Edit time interval'),
|
||||
}}
|
||||
accessType="notification"
|
||||
>
|
||||
<EditTimingRoute />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Alert, Button, LinkButton, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { MuteTimingActionsButtons } from 'app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons';
|
||||
import {
|
||||
@@ -149,7 +150,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
|
||||
const columns: Array<DynamicTableColumnProps<MuteTiming>> = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
label: t('alerting.use-columns.columns.label.name', 'Name'),
|
||||
renderCell: function renderName({ data }) {
|
||||
return (
|
||||
<div>
|
||||
@@ -164,7 +165,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
|
||||
},
|
||||
{
|
||||
id: 'timeRange',
|
||||
label: 'Time range',
|
||||
label: t('alerting.use-columns.columns.label.time-range', 'Time range'),
|
||||
renderCell: ({ data }) => {
|
||||
return renderTimeIntervals(data);
|
||||
},
|
||||
@@ -174,7 +175,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
|
||||
if (showActions) {
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
label: t('alerting.use-columns.label.actions', 'Actions'),
|
||||
alignColumn: 'end',
|
||||
renderCell: ({ data }) => (
|
||||
<MuteTimingActionsButtons muteTiming={data} alertManagerSourceName={alertManagerSourceName} />
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
|
||||
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
|
||||
|
||||
import MuteTimingForm from './MuteTimingForm';
|
||||
|
||||
function NewMuteTimingPage() {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<AlertmanagerPageWrapper
|
||||
navId="am-routes"
|
||||
pageNav={{ id: 'alert-policy-new', text: 'Add time interval' }}
|
||||
pageNav={{
|
||||
id: 'alert-policy-new',
|
||||
text: t('alerting.new-mute-timing-page.text.add-time-interval', 'Add time interval'),
|
||||
}}
|
||||
accessType="notification"
|
||||
>
|
||||
<MuteTimingForm />
|
||||
|
||||
+3
-1
@@ -70,7 +70,9 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
)}
|
||||
control={control}
|
||||
name="receiver"
|
||||
rules={{ required: { value: true, message: 'Required.' } }}
|
||||
rules={{
|
||||
required: { value: true, message: t('alerting.am-root-route-form.message.required', 'Required.') },
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<Trans i18nKey="alerting.am-root-route-form.or">or</Trans>
|
||||
|
||||
+6
-1
@@ -133,7 +133,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
|
||||
defaultValue={field.operator}
|
||||
control={control}
|
||||
name={`object_matchers.${index}.operator`}
|
||||
rules={{ required: { value: true, message: 'Required.' } }}
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.am-routes-expanded-form.message.required', 'Required.'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
|
||||
@@ -202,7 +202,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
>
|
||||
<Input
|
||||
{...register('title', {
|
||||
required: { value: true, message: 'Required.' },
|
||||
required: { value: true, message: t('alerting.template-form.message.required', 'Required.') },
|
||||
validate: { titleIsUnique },
|
||||
})}
|
||||
placeholder={t(
|
||||
|
||||
+47
-8
@@ -1,3 +1,4 @@
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import type { Monaco } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
@@ -16,12 +17,18 @@ import { SuggestionDefinition } from './suggestionDefinition';
|
||||
export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
return [
|
||||
{
|
||||
label: 'Alerts',
|
||||
kind,
|
||||
detail: 'Alert[]',
|
||||
documentation: { value: 'An Array containing all alerts' },
|
||||
documentation: {
|
||||
value: t(
|
||||
'alerting.get-global-suggestions.value.an-array-containing-all-alerts',
|
||||
'An Array containing all alerts'
|
||||
),
|
||||
},
|
||||
},
|
||||
{ label: 'Receiver', kind, detail: 'string' },
|
||||
{ label: 'Status', kind, detail: 'string' },
|
||||
@@ -32,36 +39,54 @@ export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
{ label: 'GroupKey', kind, detail: 'string' },
|
||||
{ label: 'TruncatedAlerts', kind, detail: 'integer' },
|
||||
];
|
||||
/* eslint-enable @grafana/no-untranslated-strings */
|
||||
}
|
||||
|
||||
// Suggestions that are valid only in the scope of an alert (e.g. in the .Alerts loop)
|
||||
export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
return [
|
||||
{
|
||||
label: { label: 'Status', detail: '(Alert)', description: 'string' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: { value: 'Status of the alert. It can be `firing` or `resolved`' },
|
||||
documentation: {
|
||||
value: t(
|
||||
'alerting.get-alert-suggestions.value.status-alert-firing-resolved',
|
||||
'Status of the alert. It can be `firing` or `resolved`'
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: { label: 'Labels', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: '[]KeyValue',
|
||||
documentation: { value: 'A set of labels attached to the alert.' },
|
||||
documentation: {
|
||||
value: t(
|
||||
'alerting.get-alert-suggestions.value.labels-attached-alert',
|
||||
'A set of labels attached to the alert.'
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: { label: 'Annotations', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: '[]KeyValue',
|
||||
documentation: 'A set of annotations attached to the alert.',
|
||||
documentation: t(
|
||||
'alerting.get-alert-suggestions.documentation.annotations-attached-alert',
|
||||
'A set of annotations attached to the alert.'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: { label: 'StartsAt', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'time.Time',
|
||||
documentation: 'Time the alert started firing.',
|
||||
documentation: t(
|
||||
'alerting.get-alert-suggestions.documentation.time-the-alert-started-firing',
|
||||
'Time the alert started firing.'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: { label: 'EndsAt', detail: '(Alert)' },
|
||||
@@ -74,7 +99,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
label: { label: 'GeneratorURL', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Back link to Grafana or external Alertmanager.',
|
||||
documentation: t(
|
||||
'alerting.get-alert-suggestions.documentation.grafana-external-alertmanager',
|
||||
'Back link to Grafana or external Alertmanager.'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: { label: 'SilenceURL', detail: '(Alert)' },
|
||||
@@ -99,7 +127,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
label: { label: 'Fingerprint', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'string',
|
||||
documentation: 'Fingerprint that can be used to identify the alert.',
|
||||
documentation: t(
|
||||
'alerting.get-alert-suggestions.documentation.fingerprint-identify-alert',
|
||||
'Fingerprint that can be used to identify the alert.'
|
||||
),
|
||||
},
|
||||
{
|
||||
label: { label: 'ValueString', detail: '(Alert)' },
|
||||
@@ -111,25 +142,32 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
label: { label: 'OrgID', detail: '(Alert)' },
|
||||
kind,
|
||||
detail: 'integer',
|
||||
documentation: 'The ID of the organization that owns the alert.',
|
||||
documentation: t(
|
||||
'alerting.get-alert-suggestions.documentation.organization-alert',
|
||||
'The ID of the organization that owns the alert.'
|
||||
),
|
||||
},
|
||||
];
|
||||
/* eslint-enable @grafana/no-untranslated-strings */
|
||||
}
|
||||
|
||||
// Suggestions for .Alerts
|
||||
export function getAlertsSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
return [
|
||||
{ label: 'Firing', kind, detail: 'Alert[]' },
|
||||
{ label: 'Resolved', kind, detail: 'Alert[]' },
|
||||
];
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
}
|
||||
|
||||
// Suggestions for the KeyValue types
|
||||
export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
const kind = monaco.languages.CompletionItemKind.Field;
|
||||
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
return [
|
||||
{ label: 'SortedPairs', kind, detail: '[]KeyValue' },
|
||||
{ label: 'Names', kind, detail: '[]string' },
|
||||
@@ -140,6 +178,7 @@ export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
},
|
||||
];
|
||||
/* eslint-enable @grafana/no-untranslated-strings */
|
||||
}
|
||||
|
||||
export const snippets = {
|
||||
|
||||
+2
-2
@@ -82,10 +82,10 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
|
||||
};
|
||||
const alertOptions: AlertOption[] = [
|
||||
{
|
||||
label: 'Firing',
|
||||
label: t('alerting.generate-alert-data-modal.alert-options.label.firing', 'Firing'),
|
||||
value: 'firing',
|
||||
},
|
||||
{ label: 'Resolved', value: 'resolved' },
|
||||
{ label: t('alerting.generate-alert-data-modal.alert-options.label.resolved', 'Resolved'), value: 'resolved' },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
+5
-2
@@ -123,6 +123,7 @@ interface TemplateSelectorProps {
|
||||
}
|
||||
|
||||
function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
const valueInFormIsCustom = Boolean(valueInForm) && !matchesOnlyOneTemplate(valueInForm);
|
||||
const [template, setTemplate] = useState<SelectableValue<Template> | undefined>(undefined);
|
||||
@@ -138,7 +139,10 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
|
||||
|
||||
const templateOptions: Array<SelectableValue<TemplateFieldOption>> = [
|
||||
{
|
||||
label: 'Select notification template',
|
||||
label: t(
|
||||
'alerting.template-selector.template-options.label.select-notification-template',
|
||||
'Select notification template'
|
||||
),
|
||||
ariaLabel: 'Select notification template',
|
||||
value: 'Existing',
|
||||
description: `Select an existing notification template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save saves your changes to the selected template.' : ''}`,
|
||||
@@ -156,7 +160,6 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
|
||||
setCustomTemplateValue(getUseTemplateText(template.value.name));
|
||||
}
|
||||
}, [template]);
|
||||
const { t } = useTranslate();
|
||||
|
||||
function onCustomTemplateChange(customInput: string) {
|
||||
setCustomTemplateValue(customInput);
|
||||
|
||||
@@ -66,7 +66,10 @@ export const AlertRuleNameAndMetric = () => {
|
||||
id="name"
|
||||
width={38}
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Must enter a name' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.alert-rule-name-and-metric.message.must-enter-a-name', 'Must enter a name'),
|
||||
},
|
||||
pattern: isCloudRecordingRule
|
||||
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
|
||||
: undefined,
|
||||
@@ -89,7 +92,13 @@ export const AlertRuleNameAndMetric = () => {
|
||||
id="metric"
|
||||
width={38}
|
||||
{...register('metric', {
|
||||
required: { value: true, message: 'Must enter a metric name' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.alert-rule-name-and-metric.message.must-enter-a-metric-name',
|
||||
'Must enter a metric name'
|
||||
),
|
||||
},
|
||||
pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording),
|
||||
})}
|
||||
aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')}
|
||||
@@ -130,7 +139,13 @@ export const AlertRuleNameAndMetric = () => {
|
||||
name="targetDatasourceUid"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.alert-rule-name-and-metric.message.please-select-a-data-source',
|
||||
'Please select a data source'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
import { Controller, FieldArrayWithId, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
@@ -20,6 +21,7 @@ const AnnotationHeaderField = ({
|
||||
index: number;
|
||||
labelId: string;
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const { control } = useFormContext<RuleFormValues>();
|
||||
|
||||
return (
|
||||
@@ -56,7 +58,12 @@ const AnnotationHeaderField = ({
|
||||
);
|
||||
}}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
rules={{
|
||||
required: {
|
||||
value: !!annotations[index]?.value,
|
||||
message: t('alerting.annotation-header-field.message.required', 'Required.'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</label>
|
||||
|
||||
+9
-1
@@ -40,7 +40,15 @@ export const CloudEvaluationBehavior = () => {
|
||||
<div className={styles.flexRow}>
|
||||
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
||||
<Input
|
||||
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a positive integer.' } })}
|
||||
{...register('forTime', {
|
||||
pattern: {
|
||||
value: /^\d+$/,
|
||||
message: t(
|
||||
'alerting.cloud-evaluation-behavior.message.must-be-a-positive-integer',
|
||||
'Must be a positive integer.'
|
||||
),
|
||||
},
|
||||
})}
|
||||
width={8}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -69,7 +69,10 @@ export function FolderSelector() {
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
required: { value: true, message: 'Select a folder' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<CreateNewFolder onCreate={handleFolderCreation} />
|
||||
|
||||
+24
-5
@@ -5,7 +5,7 @@ import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } fr
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import {
|
||||
Box,
|
||||
@@ -74,7 +74,7 @@ const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) =>
|
||||
const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
message: t('alerting.for-validation-options.message.required', 'Required.'),
|
||||
},
|
||||
validate: (value) => {
|
||||
// parsePrometheusDuration does not allow 0 but does allow 0s
|
||||
@@ -118,6 +118,7 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
existing: boolean;
|
||||
enableProvisionedGroups: boolean;
|
||||
}) {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||
|
||||
@@ -237,7 +238,13 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
name="group"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Must enter a group name' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.grafana-evaluation-behavior-step.message.must-enter-a-group-name',
|
||||
'Must enter a group name'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -380,7 +387,13 @@ export function GrafanaEvaluationBehaviorStep({
|
||||
)}
|
||||
id="missing-series-resolve"
|
||||
{...register('missingSeriesEvalsToResolve', {
|
||||
pattern: { value: /^\d+$/, message: 'Must be a positive integer.' },
|
||||
pattern: {
|
||||
value: /^\d+$/,
|
||||
message: t(
|
||||
'alerting.grafana-evaluation-behavior-step.message.must-be-a-positive-integer',
|
||||
'Must be a positive integer.'
|
||||
),
|
||||
},
|
||||
})}
|
||||
width={21}
|
||||
/>
|
||||
@@ -402,6 +415,7 @@ function EvaluationGroupCreationModal({
|
||||
onCreate: (group: string, evaluationInterval: string) => void;
|
||||
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
|
||||
@@ -477,7 +491,12 @@ function EvaluationGroupCreationModal({
|
||||
autoFocus={true}
|
||||
id={evaluationGroupNameId}
|
||||
placeholder={t('alerting.evaluation-group-creation-modal.placeholder-enter-a-name', 'Enter a name')}
|
||||
{...register('group', { required: { value: true, message: 'Required.' } })}
|
||||
{...register('group', {
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.evaluation-group-creation-modal.message.required', 'Required.'),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
|
||||
name="namespace"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Required.' },
|
||||
required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -105,7 +105,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
|
||||
name="group"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Required.' },
|
||||
required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -153,14 +153,27 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
*
|
||||
*/
|
||||
function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) {
|
||||
const { t } = useTranslate();
|
||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [manualRouting] = watch(['manualRouting']);
|
||||
|
||||
const routingOptions = [
|
||||
{ label: 'Select contact point', value: RoutingOptions.ContactPoint },
|
||||
{ label: 'Use notification policy', value: RoutingOptions.NotificationPolicy },
|
||||
{
|
||||
label: t(
|
||||
'alerting.manual-and-automatic-routing.routing-options.label.select-contact-point',
|
||||
'Select contact point'
|
||||
),
|
||||
value: RoutingOptions.ContactPoint,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'alerting.manual-and-automatic-routing.routing-options.label.use-notification-policy',
|
||||
'Use notification policy'
|
||||
),
|
||||
value: RoutingOptions.NotificationPolicy,
|
||||
},
|
||||
];
|
||||
|
||||
const onRoutingOptionChange = (option: RoutingOptions) => {
|
||||
|
||||
+4
-1
@@ -78,7 +78,10 @@ export function ContactPointSelector({ alertManager, onSelectContactPoint }: Con
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Contact point is required.',
|
||||
message: t(
|
||||
'alerting.contact-point-selector.message.contact-point-is-required',
|
||||
'Contact point is required.'
|
||||
),
|
||||
},
|
||||
}}
|
||||
control={control}
|
||||
|
||||
@@ -116,6 +116,7 @@ export function useCombinedLabels(
|
||||
labelsInSubform: Array<{ key: string; value: string }>,
|
||||
selectedKey: string
|
||||
) {
|
||||
const { t } = useTranslate();
|
||||
// ------- Get labels keys and their values from existing alerts
|
||||
const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName);
|
||||
// ------- Get only the keys from the ops labels, as we will fetch the values for the keys once the key is selected.
|
||||
@@ -143,12 +144,12 @@ export function useCombinedLabels(
|
||||
// create two groups of labels, one for ops and one for custom
|
||||
const groupedOptions = [
|
||||
{
|
||||
label: 'From alerts',
|
||||
label: t('alerting.use-combined-labels.grouped-options.label.from-alerts', 'From alerts'),
|
||||
options: keysFromExistingAlerts,
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
label: 'From system',
|
||||
label: t('alerting.use-combined-labels.grouped-options.label.from-system', 'From system'),
|
||||
options: keysFromGopsLabels,
|
||||
expanded: true,
|
||||
},
|
||||
@@ -372,7 +373,10 @@ export const LabelsWithoutSuggestions: FC = () => {
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.key`, {
|
||||
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||
required: {
|
||||
value: !!labels[index]?.value,
|
||||
message: t('alerting.labels-without-suggestions.message.required', 'Required.'),
|
||||
},
|
||||
})}
|
||||
placeholder={t('alerting.labels-without-suggestions.placeholder-key', 'key')}
|
||||
data-testid={`label-key-${index}`}
|
||||
@@ -387,7 +391,10 @@ export const LabelsWithoutSuggestions: FC = () => {
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.value`, {
|
||||
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||
required: {
|
||||
value: !!labels[index]?.key,
|
||||
message: t('alerting.labels-without-suggestions.message.required', 'Required.'),
|
||||
},
|
||||
})}
|
||||
placeholder={t('alerting.labels-without-suggestions.placeholder-value', 'value')}
|
||||
data-testid={`label-value-${index}`}
|
||||
|
||||
+7
-1
@@ -53,7 +53,13 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
|
||||
name="dataSourceName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.cloud-data-source-selector.message.please-select-a-data-source',
|
||||
'Please select a data source'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
+7
-1
@@ -544,7 +544,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod
|
||||
}}
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'A valid expression is required' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'alerting.query-and-expressions-step.message.a-valid-expression-is-required',
|
||||
'A valid expression is required'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMeasure } from 'react-use';
|
||||
|
||||
import { NavModelItem, UrlQueryValue } from '@grafana/data';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import {
|
||||
Alert,
|
||||
LinkButton,
|
||||
@@ -205,14 +206,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
||||
<Text variant="bodySmall">{truncatedUrl}</Text>
|
||||
);
|
||||
metadata.push({
|
||||
label: 'Runbook URL',
|
||||
label: t('alerting.create-metadata.label.runbook-url', 'Runbook URL'),
|
||||
value: valueToAdd,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDashboardAndPanel) {
|
||||
metadata.push({
|
||||
label: 'Dashboard and panel',
|
||||
label: t('alerting.create-metadata.label.dashboard-and-panel', 'Dashboard and panel'),
|
||||
value: (
|
||||
<WithReturnButton
|
||||
title={rule.name}
|
||||
@@ -226,7 +227,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
||||
});
|
||||
} else if (hasDashboard) {
|
||||
metadata.push({
|
||||
label: 'Dashboard',
|
||||
label: t('alerting.create-metadata.label.dashboard', 'Dashboard'),
|
||||
value: (
|
||||
<WithReturnButton
|
||||
title={rule.name}
|
||||
@@ -242,14 +243,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
||||
if (rulerRuleType.grafana.recordingRule(rule.rulerRule)) {
|
||||
const metric = rule.rulerRule?.grafana_alert.record?.metric ?? '';
|
||||
metadata.push({
|
||||
label: 'Metric name',
|
||||
label: t('alerting.create-metadata.label.metric-name', 'Metric name'),
|
||||
value: <Text color="primary">{metric}</Text>,
|
||||
});
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
metadata.push({
|
||||
label: 'Evaluation interval',
|
||||
label: t('alerting.create-metadata.label.evaluation-interval', 'Evaluation interval'),
|
||||
value: (
|
||||
<Text color="primary">
|
||||
<Trans i18nKey="alerting.rule-viewer.evaluation-interval">Every {{ interval }}</Trans>
|
||||
@@ -260,7 +261,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
||||
|
||||
if (hasLabels) {
|
||||
metadata.push({
|
||||
label: 'Labels',
|
||||
label: t('alerting.create-metadata.label.labels', 'Labels'),
|
||||
/* TODO truncate number of labels, maybe build in to component? */
|
||||
value: <AlertLabels labels={labels} size="sm" />,
|
||||
});
|
||||
@@ -415,14 +416,14 @@ function usePageNav(rule: CombinedRule) {
|
||||
subTitle: summary,
|
||||
children: [
|
||||
{
|
||||
text: 'Query and conditions',
|
||||
text: t('alerting.use-page-nav.page-nav.text.query-and-conditions', 'Query and conditions'),
|
||||
active: activeTab === ActiveTab.Query,
|
||||
onClick: () => {
|
||||
setActiveTab(ActiveTab.Query);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Instances',
|
||||
text: t('alerting.use-page-nav.page-nav.text.instances', 'Instances'),
|
||||
active: activeTab === ActiveTab.Instances,
|
||||
onClick: () => {
|
||||
setActiveTab(ActiveTab.Instances);
|
||||
@@ -431,7 +432,7 @@ function usePageNav(rule: CombinedRule) {
|
||||
hideFromTabs: isRecordingRuleType,
|
||||
},
|
||||
{
|
||||
text: 'History',
|
||||
text: t('alerting.use-page-nav.page-nav.text.history', 'History'),
|
||||
active: activeTab === ActiveTab.History,
|
||||
onClick: () => {
|
||||
setActiveTab(ActiveTab.History);
|
||||
@@ -440,14 +441,14 @@ function usePageNav(rule: CombinedRule) {
|
||||
hideFromTabs: !isGrafanaAlertRule,
|
||||
},
|
||||
{
|
||||
text: 'Details',
|
||||
text: t('alerting.use-page-nav.page-nav.text.details', 'Details'),
|
||||
active: activeTab === ActiveTab.Details,
|
||||
onClick: () => {
|
||||
setActiveTab(ActiveTab.Details);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Versions',
|
||||
text: t('alerting.use-page-nav.page-nav.text.versions', 'Versions'),
|
||||
active: activeTab === ActiveTab.VersionHistory,
|
||||
onClick: () => {
|
||||
setActiveTab(ActiveTab.VersionHistory);
|
||||
|
||||
@@ -259,8 +259,8 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
|
||||
</Label>
|
||||
<RadioButtonGroup<'hide'>
|
||||
options={[
|
||||
{ label: 'Show', value: undefined },
|
||||
{ label: 'Hide', value: 'hide' },
|
||||
{ label: t('alerting.rules-filter.label.show', 'Show'), value: undefined },
|
||||
{ label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' },
|
||||
]}
|
||||
value={filterState.plugins}
|
||||
onChange={(value) => updateFilters({ ...filterState, plugins: value })}
|
||||
|
||||
@@ -81,6 +81,7 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
|
||||
}
|
||||
|
||||
const FilterOptions = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Stack direction="column" alignItems="end" gap={2}>
|
||||
<Grid columns={2} gap={2} alignItems="center">
|
||||
@@ -110,11 +111,11 @@ const FilterOptions = () => {
|
||||
<RadioButtonGroup
|
||||
value={'*'}
|
||||
options={[
|
||||
{ label: 'All', value: '*' },
|
||||
{ label: 'Normal', value: 'normal' },
|
||||
{ label: 'Pending', value: 'pending' },
|
||||
{ label: 'Recovering', value: 'recovering' },
|
||||
{ label: 'Firing', value: 'firing' },
|
||||
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
|
||||
{ label: t('alerting.filter-options.label.normal', 'Normal'), value: 'normal' },
|
||||
{ label: t('alerting.filter-options.label.pending', 'Pending'), value: 'pending' },
|
||||
{ label: t('alerting.filter-options.label.recovering', 'Recovering'), value: 'recovering' },
|
||||
{ label: t('alerting.filter-options.label.firing', 'Firing'), value: 'firing' },
|
||||
]}
|
||||
/>
|
||||
<Label>
|
||||
@@ -123,9 +124,9 @@ const FilterOptions = () => {
|
||||
<RadioButtonGroup
|
||||
value={'*'}
|
||||
options={[
|
||||
{ label: 'All', value: '*' },
|
||||
{ label: 'Alert rule', value: 'alerting' },
|
||||
{ label: 'Recording rule', value: 'recording' },
|
||||
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
|
||||
{ label: t('alerting.filter-options.label.alert-rule', 'Alert rule'), value: 'alerting' },
|
||||
{ label: t('alerting.filter-options.label.recording-rule', 'Recording rule'), value: 'recording' },
|
||||
]}
|
||||
/>
|
||||
<Label>
|
||||
@@ -134,10 +135,10 @@ const FilterOptions = () => {
|
||||
<RadioButtonGroup
|
||||
value={'*'}
|
||||
options={[
|
||||
{ label: 'All', value: '*' },
|
||||
{ label: 'OK', value: 'ok' },
|
||||
{ label: 'No data', value: 'no_data' },
|
||||
{ label: 'Error', value: 'error' },
|
||||
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
|
||||
{ label: t('alerting.filter-options.label.ok', 'OK'), value: 'ok' },
|
||||
{ label: t('alerting.filter-options.label.no-data', 'No data'), value: 'no_data' },
|
||||
{ label: t('alerting.filter-options.label.error', 'Error'), value: 'error' },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isUnsignedPluginSignature,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { ExpressionDatasourceRef } from '@grafana/runtime/internal';
|
||||
import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui';
|
||||
@@ -20,6 +21,7 @@ export interface MultipleDataSourcePickerProps extends Omit<DataSourcePickerProp
|
||||
}
|
||||
|
||||
export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) => {
|
||||
const { t } = useTranslate();
|
||||
const dataSourceSrv = getDataSourceSrv();
|
||||
|
||||
const [state, setState] = useState<DataSourcePickerState>();
|
||||
@@ -121,8 +123,22 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) =
|
||||
}));
|
||||
|
||||
const groupedOptions = [
|
||||
{ label: 'Data sources with configured alert rules', options: alertManagingDs, expanded: true },
|
||||
{ label: 'Other data sources', options: nonAlertManagingDs, expanded: true },
|
||||
{
|
||||
label: t(
|
||||
'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.data-sources-with-configured-alert-rules',
|
||||
'Data sources with configured alert rules'
|
||||
),
|
||||
options: alertManagingDs,
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.other-data-sources',
|
||||
'Other data sources'
|
||||
),
|
||||
options: nonAlertManagingDs,
|
||||
expanded: true,
|
||||
},
|
||||
];
|
||||
|
||||
return groupedOptions;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { Pagination, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
|
||||
@@ -192,17 +193,18 @@ function useColumns(
|
||||
showNextEvaluationColumn: boolean,
|
||||
isRulerLoading: boolean
|
||||
) {
|
||||
const { t } = useTranslate();
|
||||
return useMemo((): RuleTableColumnProps[] => {
|
||||
const columns: RuleTableColumnProps[] = [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
label: t('alerting.use-columns.columns.label.state', 'State'),
|
||||
renderCell: ({ data: rule }) => <RuleStateCell rule={rule} />,
|
||||
size: '165px',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
label: t('alerting.use-columns.columns.label.name', 'Name'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => rule.name,
|
||||
size: showNextEvaluationColumn ? 4 : 5,
|
||||
@@ -237,7 +239,7 @@ function useColumns(
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
label: 'Health',
|
||||
label: t('alerting.use-columns.columns.label.health', 'Health'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { promRule, group } }) => (promRule ? <RuleHealth rule={promRule} /> : null),
|
||||
size: '75px',
|
||||
@@ -246,7 +248,7 @@ function useColumns(
|
||||
if (showSummaryColumn) {
|
||||
columns.push({
|
||||
id: 'summary',
|
||||
label: 'Summary',
|
||||
label: t('alerting.use-columns.label.summary', 'Summary'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => {
|
||||
return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />;
|
||||
@@ -258,7 +260,7 @@ function useColumns(
|
||||
if (showNextEvaluationColumn) {
|
||||
columns.push({
|
||||
id: 'nextEvaluation',
|
||||
label: 'Next evaluation',
|
||||
label: t('alerting.use-columns.label.next-evaluation', 'Next evaluation'),
|
||||
renderCell: ({ data: rule }) => {
|
||||
const nextEvalInfo = calculateNextEvaluationEstimate(rule.promRule?.lastEvaluation, rule.group.interval);
|
||||
|
||||
@@ -282,7 +284,7 @@ function useColumns(
|
||||
if (showGroupColumn) {
|
||||
columns.push({
|
||||
id: 'group',
|
||||
label: 'Group',
|
||||
label: t('alerting.use-columns.label.group', 'Group'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => {
|
||||
const { namespace, group } = rule;
|
||||
@@ -301,14 +303,14 @@ function useColumns(
|
||||
}
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
label: t('alerting.use-columns.label.actions', 'Actions'),
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => <RuleActionsCell rule={rule} isLoadingRuler={isRulerLoading} />,
|
||||
size: '215px',
|
||||
});
|
||||
|
||||
return columns;
|
||||
}, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isRulerLoading]);
|
||||
}, [t, showNextEvaluationColumn, showSummaryColumn, showGroupColumn, isRulerLoading]);
|
||||
}
|
||||
|
||||
function RuleStateCell({ rule }: { rule: CombinedRule }) {
|
||||
|
||||
+11
-4
@@ -67,6 +67,7 @@ export const StateFilterValues = {
|
||||
|
||||
export const CentralAlertHistoryScene = () => {
|
||||
//track the loading of the central alert state history
|
||||
const { t } = useTranslate();
|
||||
useEffect(() => {
|
||||
logInfo(LogMessages.loadedCentralAlertStateHistory);
|
||||
}, []);
|
||||
@@ -78,14 +79,17 @@ export const CentralAlertHistoryScene = () => {
|
||||
// textbox variable for filtering by labels
|
||||
const labelsFilterVariable = new TextBoxVariable({
|
||||
name: LABELS_FILTER,
|
||||
label: 'Labels: ',
|
||||
label: t('alerting.central-alert-history-scene.scene.labels-filter-variable.label.labels', 'Labels: '),
|
||||
});
|
||||
|
||||
//custom variable for filtering by the current state
|
||||
const transitionsToFilterVariable = new CustomVariable({
|
||||
name: STATE_FILTER_TO,
|
||||
value: StateFilterValues.all,
|
||||
label: 'End state:',
|
||||
label: t(
|
||||
'alerting.central-alert-history-scene.scene.transitions-to-filter-variable.label.end-state',
|
||||
'End state:'
|
||||
),
|
||||
hide: VariableHide.dontHide,
|
||||
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending},To Recovering : ${StateFilterValues.recovering}`,
|
||||
});
|
||||
@@ -94,7 +98,10 @@ export const CentralAlertHistoryScene = () => {
|
||||
const transitionsFromFilterVariable = new CustomVariable({
|
||||
name: STATE_FILTER_FROM,
|
||||
value: StateFilterValues.all,
|
||||
label: 'Start state:',
|
||||
label: t(
|
||||
'alerting.central-alert-history-scene.scene.transitions-from-filter-variable.label.start-state',
|
||||
'Start state:'
|
||||
),
|
||||
hide: VariableHide.dontHide,
|
||||
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending},From Recovering : ${StateFilterValues.recovering}`,
|
||||
});
|
||||
@@ -132,7 +139,7 @@ export const CentralAlertHistoryScene = () => {
|
||||
],
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// we need to call this to sync the url with the scene state
|
||||
const isUrlSyncInitialized = useUrlSync(scene);
|
||||
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { RuntimeDataSource, sceneUtils } from '@grafana/scenes';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
@@ -72,7 +73,11 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
|
||||
}
|
||||
|
||||
testDatasource(): Promise<TestDataSourceResponse> {
|
||||
return Promise.resolve({ status: 'success', message: 'Data source is working', title: 'Success' });
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: t('alerting.history-apidatasource.message.data-source-is-working', 'Data source is working'),
|
||||
title: t('alerting.history-apidatasource.title.success', 'Success'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-6
@@ -2,6 +2,7 @@ import { memo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { VisibilityMode } from '@grafana/schema';
|
||||
import { LegendDisplayMode, useTheme2 } from '@grafana/ui';
|
||||
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
|
||||
@@ -16,6 +17,7 @@ interface LogTimelineViewerProps {
|
||||
const replaceVariables: InterpolateFunction = (v) => v;
|
||||
|
||||
export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerProps) => {
|
||||
const { t } = useTranslate();
|
||||
const theme = useTheme2();
|
||||
|
||||
return (
|
||||
@@ -38,12 +40,36 @@ export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerP
|
||||
showLegend: true,
|
||||
}}
|
||||
legendItems={[
|
||||
{ label: 'Normal', color: theme.colors.success.main, yAxis: 1 },
|
||||
{ label: 'Pending', color: theme.colors.warning.main, yAxis: 1 },
|
||||
{ label: 'Recovering', color: theme.colors.warning.main, yAxis: 1 },
|
||||
{ label: 'Firing', color: theme.colors.error.main, yAxis: 1 },
|
||||
{ label: 'No Data', color: theme.colors.info.main, yAxis: 1 },
|
||||
{ label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 },
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.normal', 'Normal'),
|
||||
color: theme.colors.success.main,
|
||||
yAxis: 1,
|
||||
},
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.pending', 'Pending'),
|
||||
color: theme.colors.warning.main,
|
||||
yAxis: 1,
|
||||
},
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.recovering', 'Recovering'),
|
||||
color: theme.colors.warning.main,
|
||||
yAxis: 1,
|
||||
},
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.firing', 'Firing'),
|
||||
color: theme.colors.error.main,
|
||||
yAxis: 1,
|
||||
},
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.no-data', 'No Data'),
|
||||
color: theme.colors.info.main,
|
||||
yAxis: 1,
|
||||
},
|
||||
{
|
||||
label: t('alerting.log-timeline-viewer.label.mixed', 'Mixed'),
|
||||
color: theme.colors.text.secondary,
|
||||
yAxis: 1,
|
||||
},
|
||||
]}
|
||||
replaceVariables={replaceVariables}
|
||||
/>
|
||||
|
||||
+12
-2
@@ -59,9 +59,19 @@ const StateHistory = ({ ruleUID }: Props) => {
|
||||
}
|
||||
|
||||
const columns: Array<DynamicTableColumnProps<StateHistoryRowItem>> = [
|
||||
{ id: 'state', label: 'State', size: 'max-content', renderCell: renderStateCell },
|
||||
{
|
||||
id: 'state',
|
||||
label: t('alerting.state-history.columns.label.state', 'State'),
|
||||
size: 'max-content',
|
||||
renderCell: renderStateCell,
|
||||
},
|
||||
{ id: 'value', label: '', size: 'auto', renderCell: renderValueCell },
|
||||
{ id: 'timestamp', label: 'Time', size: 'max-content', renderCell: renderTimestampCell },
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: t('alerting.state-history.columns.label.time', 'Time'),
|
||||
size: 'max-content',
|
||||
renderCell: renderTimestampCell,
|
||||
},
|
||||
];
|
||||
|
||||
// group the state history list by unique set of labels
|
||||
|
||||
@@ -80,7 +80,10 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave
|
||||
// manually register the config field with validation
|
||||
// @TODO sometimes the value doesn't get registered – find out why
|
||||
register('configJSON', {
|
||||
required: { value: true, message: 'Configuration cannot be empty' },
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.alertmanager-config.message.configuration-cannot-be-empty', 'Configuration cannot be empty'),
|
||||
},
|
||||
validate: (value: string) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
|
||||
@@ -63,7 +63,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
|
||||
>
|
||||
<Input
|
||||
{...register(`matchers.${index}.name` as const, {
|
||||
required: { value: required, message: 'Required.' },
|
||||
required: {
|
||||
value: required,
|
||||
message: t('alerting.matchers-field.message.required', 'Required.'),
|
||||
},
|
||||
})}
|
||||
defaultValue={matcher.name}
|
||||
placeholder={t('alerting.matchers-field.placeholder-label', 'label')}
|
||||
@@ -85,7 +88,12 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
|
||||
)}
|
||||
defaultValue={matcher.operator || matcherFieldOptions[0].value}
|
||||
name={`matchers.${index}.operator`}
|
||||
rules={{ required: { value: required, message: 'Required.' } }}
|
||||
rules={{
|
||||
required: {
|
||||
value: required,
|
||||
message: t('alerting.matchers-field.message.required', 'Required.'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
@@ -95,7 +103,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
|
||||
>
|
||||
<Input
|
||||
{...register(`matchers.${index}.value` as const, {
|
||||
required: { value: required, message: 'Required.' },
|
||||
required: {
|
||||
value: required,
|
||||
message: t('alerting.matchers-field.message.required', 'Required.'),
|
||||
},
|
||||
})}
|
||||
defaultValue={matcher.value}
|
||||
placeholder={t('alerting.matchers-field.placeholder-value', 'value')}
|
||||
|
||||
+4
-3
@@ -148,12 +148,13 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
|
||||
};
|
||||
|
||||
function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
label: t('alerting.use-columns.label.state', 'State'),
|
||||
renderCell: function renderStateTag({ data }) {
|
||||
return <AmAlertStateTag state={data.status.state} />;
|
||||
},
|
||||
@@ -162,7 +163,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
label: t('alerting.use-columns.label.labels', 'Labels'),
|
||||
renderCell: function renderName({ data }) {
|
||||
return <AlertLabels labels={data.labels} size="sm" />;
|
||||
},
|
||||
@@ -170,7 +171,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
label: 'Created',
|
||||
label: t('alerting.use-columns.label.created', 'Created'),
|
||||
renderCell: function renderSummary({ data }) {
|
||||
return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}</>;
|
||||
},
|
||||
|
||||
@@ -254,7 +254,9 @@ export const SilencesEditor = ({
|
||||
invalid={!!formState.errors.comment}
|
||||
>
|
||||
<TextArea
|
||||
{...register('comment', { required: { value: true, message: 'Required.' } })}
|
||||
{...register('comment', {
|
||||
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
|
||||
})}
|
||||
rows={5}
|
||||
placeholder={t(
|
||||
'alerting.silences-editor.comment-placeholder-details-about-the-silence',
|
||||
@@ -271,7 +273,9 @@ export const SilencesEditor = ({
|
||||
invalid={!!formState.errors.createdBy}
|
||||
>
|
||||
<Input
|
||||
{...register('createdBy', { required: { value: true, message: 'Required.' } })}
|
||||
{...register('createdBy', {
|
||||
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
|
||||
})}
|
||||
placeholder={t(
|
||||
'alerting.silences-editor.placeholder-whos-creating-the-silence',
|
||||
"Who's creating the silence"
|
||||
@@ -321,9 +325,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
});
|
||||
|
||||
function ExistingSilenceEditorPage() {
|
||||
const { t } = useTranslate();
|
||||
const pageNav = {
|
||||
id: 'silence-edit',
|
||||
text: 'Edit silence',
|
||||
text: t('alerting.existing-silence-editor-page.page-nav.text.edit-silence', 'Edit silence'),
|
||||
subTitle: 'Recreate existing silence to stop notifications from a particular alert rule',
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -293,7 +293,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
const columns: SilenceTableColumnProps[] = [
|
||||
{
|
||||
id: 'state',
|
||||
label: 'State',
|
||||
label: t('alerting.use-columns.columns.label.state', 'State'),
|
||||
renderCell: function renderStateTag({ data: { status } }) {
|
||||
return <SilenceStateTag state={status.state} />;
|
||||
},
|
||||
@@ -301,7 +301,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
},
|
||||
{
|
||||
id: 'alert-rule',
|
||||
label: 'Alert rule targeted',
|
||||
label: t('alerting.use-columns.columns.label.alert-rule-targeted', 'Alert rule targeted'),
|
||||
renderCell: function renderAlertRuleLink({ data: { metadata } }) {
|
||||
return metadata?.rule_title ? (
|
||||
<Link
|
||||
@@ -317,7 +317,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
},
|
||||
{
|
||||
id: 'matchers',
|
||||
label: 'Matching labels',
|
||||
label: t('alerting.use-columns.columns.label.matching-labels', 'Matching labels'),
|
||||
renderCell: function renderMatchers({ data: { matchers } }) {
|
||||
const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || [];
|
||||
return <Matchers matchers={filteredMatchers} />;
|
||||
@@ -326,7 +326,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
label: 'Alerts silenced',
|
||||
label: t('alerting.use-columns.columns.label.alerts-silenced', 'Alerts silenced'),
|
||||
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
|
||||
return (
|
||||
<span data-testid="alerts">
|
||||
@@ -341,7 +341,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
},
|
||||
{
|
||||
id: 'schedule',
|
||||
label: 'Schedule',
|
||||
label: t('alerting.use-columns.columns.label.schedule', 'Schedule'),
|
||||
renderCell: function renderSchedule({ data: { startsAt, endsAt } }) {
|
||||
const startsAtDate = dateMath.parse(startsAt);
|
||||
const endsAtDate = dateMath.parse(endsAt);
|
||||
@@ -354,7 +354,7 @@ function useColumns(alertManagerSourceName: string) {
|
||||
if (updateSupported) {
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
label: t('alerting.use-columns.label.actions', 'Actions'),
|
||||
renderCell: function renderActions({ data: silence }) {
|
||||
const isExpired = silence.status.state === SilenceState.Expired;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FieldValues, RegisterOptions } from 'react-hook-form';
|
||||
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { MIN_TIME_RANGE_STEP_S } from '../components/rule-editor/GrafanaEvaluationBehavior';
|
||||
@@ -10,7 +11,7 @@ import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusD
|
||||
export const evaluateEveryValidationOptions = <T extends FieldValues>(rules: RulerRuleDTO[]): RegisterOptions<T> => ({
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
message: t('alerting.evaluate-every-validation-options.message.required', 'Required.'),
|
||||
},
|
||||
validate: (evaluateEvery: string) => {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import {
|
||||
EmbeddedScene,
|
||||
@@ -214,7 +215,7 @@ export function getInsightsScenes() {
|
||||
|
||||
function getGrafanaManagedScenes() {
|
||||
return new NestedScene({
|
||||
title: 'Grafana-managed alert rules',
|
||||
title: t('alerting.get-grafana-managed-scenes.title.grafanamanaged-alert-rules', 'Grafana-managed alert rules'),
|
||||
canCollapse: true,
|
||||
isCollapsed: false,
|
||||
body: new SceneFlexLayout({
|
||||
@@ -304,7 +305,7 @@ function getGrafanaManagedScenes() {
|
||||
|
||||
function getGrafanaAlertmanagerScenes() {
|
||||
return new NestedScene({
|
||||
title: 'Grafana Alertmanager',
|
||||
title: t('alerting.get-grafana-alertmanager-scenes.title.grafana-alertmanager', 'Grafana Alertmanager'),
|
||||
canCollapse: true,
|
||||
isCollapsed: false,
|
||||
body: new SceneFlexLayout({
|
||||
@@ -327,7 +328,7 @@ function getGrafanaAlertmanagerScenes() {
|
||||
|
||||
function getCloudScenes() {
|
||||
return new NestedScene({
|
||||
title: 'Mimir Alertmanager',
|
||||
title: t('alerting.get-cloud-scenes.title.mimir-alertmanager', 'Mimir Alertmanager'),
|
||||
canCollapse: true,
|
||||
isCollapsed: false,
|
||||
body: new SceneFlexLayout({
|
||||
@@ -361,7 +362,7 @@ function getCloudScenes() {
|
||||
|
||||
function getMimirManagedRulesScenes() {
|
||||
return new NestedScene({
|
||||
title: 'Mimir-managed alert rules',
|
||||
title: t('alerting.get-mimir-managed-rules-scenes.title.mimirmanaged-alert-rules', 'Mimir-managed alert rules'),
|
||||
canCollapse: true,
|
||||
isCollapsed: false,
|
||||
body: new SceneFlexLayout({
|
||||
@@ -402,14 +403,17 @@ function getMimirManagedRulesScenes() {
|
||||
|
||||
function getMimirManagedRulesPerGroupScenes() {
|
||||
const ruleGroupHandler = new QueryVariable({
|
||||
label: 'Rule Group',
|
||||
label: t('alerting.get-mimir-managed-rules-per-group-scenes.rule-group-handler.label.rule-group', 'Rule Group'),
|
||||
name: 'rule_group',
|
||||
datasource: cloudUsageDs,
|
||||
query: 'label_values(grafanacloud_instance_rule_group_rules,rule_group)',
|
||||
});
|
||||
|
||||
return new NestedScene({
|
||||
title: 'Mimir-managed alert rules - per rule group',
|
||||
title: t(
|
||||
'alerting.get-mimir-managed-rules-per-group-scenes.title.mimirmanaged-alert-rules-per-rule-group',
|
||||
'Mimir-managed alert rules - per rule group'
|
||||
),
|
||||
canCollapse: true,
|
||||
isCollapsed: false,
|
||||
body: new SceneFlexLayout({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DataQueryRequest, DataQueryResponse, DataQueryResponseData, TestDataSourceResponse } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import {
|
||||
PanelBuilders,
|
||||
@@ -37,7 +38,11 @@ class LokiAPIDatasource extends RuntimeDataSource {
|
||||
}
|
||||
|
||||
testDatasource(): Promise<TestDataSourceResponse> {
|
||||
return Promise.resolve({ status: 'success', message: 'Data source is working', title: 'Success' });
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: t('alerting.loki-apidatasource.message.data-source-is-working', 'Data source is working'),
|
||||
title: t('alerting.loki-apidatasource.title.success', 'Success'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { AppPluginConfig, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Badge, Stack, Text } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound';
|
||||
@@ -16,8 +17,11 @@ import { SSOProvider } from './types';
|
||||
const getPageNav = (config?: SSOProvider): NavModelItem => {
|
||||
if (!config) {
|
||||
return {
|
||||
text: 'Authentication',
|
||||
subTitle: 'Configure authentication providers',
|
||||
text: t('auth-config.get-page-nav.text.authentication', 'Authentication'),
|
||||
subTitle: t(
|
||||
'auth-config.get-page-nav.subTitle.configure-authentication-providers',
|
||||
'Configure authentication providers'
|
||||
),
|
||||
icon: 'shield',
|
||||
id: 'authentication',
|
||||
};
|
||||
|
||||
@@ -313,7 +313,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
|
||||
),
|
||||
multi: false,
|
||||
options: clientAuthenticationOptions(provider),
|
||||
defaultValue: { value: 'none', label: 'None' },
|
||||
defaultValue: { value: 'none', label: t('auth-config.field-map.label.none', 'None') },
|
||||
validation: {
|
||||
required: true,
|
||||
message: t('auth-config.fields.required', 'This field is required'),
|
||||
@@ -405,11 +405,13 @@ export function fieldMap(provider: string): Record<string, FieldData> {
|
||||
),
|
||||
multi: false,
|
||||
options: [
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
{ value: 'AutoDetect', label: 'AutoDetect' },
|
||||
{ value: 'InParams', label: 'InParams' },
|
||||
{ value: 'InHeader', label: 'InHeader' },
|
||||
],
|
||||
defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },
|
||||
/* eslint-enable @grafana/no-untranslated-strings */
|
||||
},
|
||||
tokenUrl: {
|
||||
label: tokenURLLabel,
|
||||
@@ -884,6 +886,7 @@ function orgMappingDescription(provider: string): string {
|
||||
|
||||
function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> {
|
||||
// Other options are purposefully not translated
|
||||
/* eslint-disable @grafana/no-untranslated-strings */
|
||||
switch (provider) {
|
||||
case 'azuread':
|
||||
return [
|
||||
@@ -898,4 +901,5 @@ function clientAuthenticationOptions(provider: string): Array<SelectableValue<st
|
||||
{ value: 'client_secret_post', label: 'Client secret' },
|
||||
];
|
||||
}
|
||||
/* eslint-enable @grafana/no-untranslated-strings */
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PluginState } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { TextDimensionMode } from '@grafana/schema';
|
||||
import { Button, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
@@ -194,9 +195,9 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.button-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.button-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.button-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -206,7 +207,7 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.button-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -153,9 +154,9 @@ export const cloudItem: CanvasElementItem = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.cloud-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.cloud-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.cloud-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -166,9 +167,9 @@ export const cloudItem: CanvasElementItem = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.cloud-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.cloud-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.cloud-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -178,7 +179,7 @@ export const cloudItem: CanvasElementItem = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.cloud-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -159,9 +160,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.ellipse-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.ellipse-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.ellipse-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -172,9 +173,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.ellipse-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.ellipse-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.ellipse-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -184,7 +185,7 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.ellipse-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useObservable } from 'react-use';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { TextDimensionMode } from '@grafana/schema';
|
||||
import { usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { FieldNamePicker, frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/internal';
|
||||
@@ -220,9 +221,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.metric-value-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.metric-value-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.metric-value-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -233,9 +234,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.metric-value-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.metric-value-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.metric-value-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -245,7 +246,7 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.metric-value-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -153,9 +154,9 @@ export const parallelogramItem: CanvasElementItem = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.parallelogram-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.parallelogram-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.parallelogram-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -166,9 +167,9 @@ export const parallelogramItem: CanvasElementItem = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.parallelogram-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.parallelogram-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.parallelogram-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -178,7 +179,7 @@ export const parallelogramItem: CanvasElementItem = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.parallelogram-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
@@ -120,9 +121,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.rectangle-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.rectangle-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.rectangle-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -133,9 +134,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.rectangle-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.rectangle-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.rectangle-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -145,7 +146,7 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.rectangle-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { of } from 'rxjs';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -197,9 +198,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.text-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.text-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.text-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -210,9 +211,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.text-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.text-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.text-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -222,7 +223,7 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.text-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -154,9 +155,9 @@ export const triangleItem: CanvasElementItem = {
|
||||
name: 'Align text',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: Align.Left, label: 'Left' },
|
||||
{ value: Align.Center, label: 'Center' },
|
||||
{ value: Align.Right, label: 'Right' },
|
||||
{ value: Align.Left, label: t('canvas.triangle-item.label.left', 'Left') },
|
||||
{ value: Align.Center, label: t('canvas.triangle-item.label.center', 'Center') },
|
||||
{ value: Align.Right, label: t('canvas.triangle-item.label.right', 'Right') },
|
||||
],
|
||||
},
|
||||
defaultValue: Align.Left,
|
||||
@@ -167,9 +168,9 @@ export const triangleItem: CanvasElementItem = {
|
||||
name: 'Vertical align',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: VAlign.Top, label: 'Top' },
|
||||
{ value: VAlign.Middle, label: 'Middle' },
|
||||
{ value: VAlign.Bottom, label: 'Bottom' },
|
||||
{ value: VAlign.Top, label: t('canvas.triangle-item.label.top', 'Top') },
|
||||
{ value: VAlign.Middle, label: t('canvas.triangle-item.label.middle', 'Middle') },
|
||||
{ value: VAlign.Bottom, label: t('canvas.triangle-item.label.bottom', 'Bottom') },
|
||||
],
|
||||
},
|
||||
defaultValue: VAlign.Middle,
|
||||
@@ -179,7 +180,7 @@ export const triangleItem: CanvasElementItem = {
|
||||
path: 'config.size',
|
||||
name: 'Text size',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
placeholder: t('canvas.triangle-item.placeholder.auto', 'Auto'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLocation, useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModel, NavModelItem } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks';
|
||||
@@ -9,6 +10,7 @@ import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
export function useDataSourceSettingsNav(pageIdParam?: string) {
|
||||
const { t } = useTranslate();
|
||||
const { uid = '' } = useParams<{ uid: string }>();
|
||||
const location = useLocation();
|
||||
const datasource = useDataSource(uid);
|
||||
@@ -27,17 +29,17 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
|
||||
const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-settings-${uid}`;
|
||||
let pageNav: NavModel = {
|
||||
node: {
|
||||
text: 'Data Source Nav Node',
|
||||
text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
|
||||
},
|
||||
main: {
|
||||
text: 'Data Source Nav Node',
|
||||
text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
|
||||
},
|
||||
};
|
||||
|
||||
if (loadError) {
|
||||
const node: NavModelItem = {
|
||||
text: loadError,
|
||||
subTitle: 'Data Source Error',
|
||||
subTitle: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'),
|
||||
icon: 'exclamation-triangle',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { Alert, Badge, TextLink } from '@grafana/ui';
|
||||
import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage';
|
||||
import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
|
||||
@@ -8,6 +8,7 @@ import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
|
||||
import { ROUTES } from '../constants';
|
||||
|
||||
export function DataSourceDetailsPage() {
|
||||
const { t } = useTranslate();
|
||||
const overrideNavId = 'standalone-plugin-page-/connections/add-new-connection';
|
||||
const { id = '' } = useParams<{ id: string }>();
|
||||
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
||||
@@ -20,8 +21,11 @@ export function DataSourceDetailsPage() {
|
||||
navId={navId}
|
||||
notFoundComponent={<NotFoundDatasource />}
|
||||
notFoundNavModel={{
|
||||
text: 'Unknown datasource',
|
||||
subTitle: 'No datasource with this ID could be found.',
|
||||
text: t('connections.data-source-details-page.text.unknown-datasource', 'Unknown datasource'),
|
||||
subTitle: t(
|
||||
'connections.data-source-details-page.subTitle.datasource-could-found',
|
||||
'No datasource with this ID could be found.'
|
||||
),
|
||||
active: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { NewDataSource } from 'app/features/datasources/components/NewDataSource';
|
||||
|
||||
export function NewDataSourcePage() {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Page
|
||||
navId={'connections-datasources'}
|
||||
pageNav={{ text: 'Add data source', subTitle: 'Choose a data source type', active: true }}
|
||||
pageNav={{
|
||||
text: t('connections.new-data-source-page.text.add-data-source', 'Add data source'),
|
||||
subTitle: t('connections.new-data-source-page.subTitle.choose-a-data-source-type', 'Choose a data source type'),
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<Page.Contents>
|
||||
<NewDataSource />
|
||||
|
||||
@@ -69,9 +69,12 @@ export function AddNewConnection() {
|
||||
);
|
||||
const { t } = useTranslate();
|
||||
const filterByOptions = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'installed', label: 'Installed' },
|
||||
{ value: 'has-update', label: 'New Updates' },
|
||||
{ value: 'all', label: t('connections.add-new-connection.filter-by-options.label.all', 'All') },
|
||||
{ value: 'installed', label: t('connections.add-new-connection.filter-by-options.label.installed', 'Installed') },
|
||||
{
|
||||
value: 'has-update',
|
||||
label: t('connections.add-new-connection.filter-by-options.label.new-updates', 'New Updates'),
|
||||
},
|
||||
];
|
||||
|
||||
const onClickCardGridItem = (e: MouseEvent<HTMLElement>, item: CardGridItem) => {
|
||||
@@ -160,11 +163,17 @@ export function AddNewConnection() {
|
||||
value={sortBy?.toString()}
|
||||
onChange={onSortByChange}
|
||||
options={[
|
||||
{ value: 'nameAsc', label: 'By name (A-Z)' },
|
||||
{ value: 'nameDesc', label: 'By name (Z-A)' },
|
||||
{ value: 'updated', label: 'By updated date' },
|
||||
{ value: 'published', label: 'By published date' },
|
||||
{ value: 'downloads', label: 'By downloads' },
|
||||
{ value: 'nameAsc', label: t('connections.add-new-connection.label.by-name-az', 'By name (A-Z)') },
|
||||
{ value: 'nameDesc', label: t('connections.add-new-connection.label.by-name-za', 'By name (Z-A)') },
|
||||
{
|
||||
value: 'updated',
|
||||
label: t('connections.add-new-connection.label.by-updated-date', 'By updated date'),
|
||||
},
|
||||
{
|
||||
value: 'published',
|
||||
label: t('connections.add-new-connection.label.by-published-date', 'By published date'),
|
||||
},
|
||||
{ value: 'downloads', label: t('connections.add-new-connection.label.by-downloads', 'By downloads') },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -66,14 +66,14 @@ export function AddToDashboardForm<TOptions = undefined>({
|
||||
|
||||
if (canCreateDashboard) {
|
||||
saveTargets.push({
|
||||
label: 'New dashboard',
|
||||
label: t('dashboard-scene.add-to-dashboard-form.label.new-dashboard', 'New dashboard'),
|
||||
value: SaveTarget.NewDashboard,
|
||||
});
|
||||
}
|
||||
|
||||
if (canWriteDashboard) {
|
||||
saveTargets.push({
|
||||
label: 'Existing dashboard',
|
||||
label: t('dashboard-scene.add-to-dashboard-form.label.existing-dashboard', 'Existing dashboard'),
|
||||
value: SaveTarget.ExistingDashboard,
|
||||
});
|
||||
}
|
||||
@@ -154,7 +154,15 @@ export function AddToDashboardForm<TOptions = undefined>({
|
||||
control={control}
|
||||
name="dashboardUid"
|
||||
shouldUnregister
|
||||
rules={{ required: { value: true, message: 'This field is required.' } }}
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: t(
|
||||
'dashboard-scene.add-to-dashboard-form.message.this-field-is-required',
|
||||
'This field is required.'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { locationUtil, TimeRange } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { Panel } from '@grafana/schema';
|
||||
import store from 'app/core/store';
|
||||
@@ -56,7 +57,10 @@ export function addToDashboard({
|
||||
} catch {
|
||||
return {
|
||||
error: AddToDashboardError.SET_DASHBOARD_LS,
|
||||
message: 'Could not add panel to dashboard. Please try again.',
|
||||
message: t(
|
||||
'dashboard-scene.add-to-dashboard.message.could-panel-dashboard-please-again',
|
||||
'Could not add panel to dashboard. Please try again.'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,7 +73,10 @@ export function addToDashboard({
|
||||
store.delete(DASHBOARD_FROM_LS_KEY);
|
||||
return {
|
||||
error: GenericError.NAVIGATION,
|
||||
message: 'Could not navigate to the selected dashboard. Please try again.',
|
||||
message: t(
|
||||
'dashboard-scene.add-to-dashboard.message.could-navigate-selected-dashboard-please-again',
|
||||
'Could not navigate to the selected dashboard. Please try again.'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { Box } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { EmbeddedDashboard } from './EmbeddedDashboard';
|
||||
|
||||
export function EmbeddedDashboardTestPage() {
|
||||
const { t } = useTranslate();
|
||||
const [state, setState] = useState('?from=now-5m&to=now');
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="dashboards/browse"
|
||||
pageNav={{ text: 'Embedding dashboard', subTitle: 'Showing dashboard: Panel Tests - Pie chart' }}
|
||||
pageNav={{
|
||||
text: t('dashboard-scene.embedded-dashboard-test-page.text.embedding-dashboard', 'Embedding dashboard'),
|
||||
subTitle: t(
|
||||
'dashboard-scene.embedded-dashboard-test-page.subTitle.showing-dashboard-panel-tests-pie-chart',
|
||||
'Showing dashboard: Panel Tests - Pie chart'
|
||||
),
|
||||
}}
|
||||
layout={PageLayoutType.Canvas}
|
||||
>
|
||||
{/* this is a test page, no need to translate */}
|
||||
|
||||
@@ -63,8 +63,8 @@ export function HelpWizard({ panel, onClose }: Props) {
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Snapshot', value: SnapshotTab.Support },
|
||||
{ label: 'Data', value: SnapshotTab.Data },
|
||||
{ label: t('dashboard-scene.help-wizard.tabs.label.snapshot', 'Snapshot'), value: SnapshotTab.Support },
|
||||
{ label: t('dashboard-scene.help-wizard.tabs.label.data', 'Data'), value: SnapshotTab.Data },
|
||||
];
|
||||
|
||||
const hasSupportBundleAccess =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import saveAs from 'file-saver';
|
||||
|
||||
import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
|
||||
@@ -54,13 +55,16 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
|
||||
snapshotUpdate: 0,
|
||||
options: [
|
||||
{
|
||||
label: 'GitHub comment',
|
||||
label: t('dashboard-scene.support-snapshot-service.label.git-hub-comment', 'GitHub comment'),
|
||||
description: 'Copy and paste this message into a GitHub issue or comment',
|
||||
value: ShowMessage.GithubComment,
|
||||
},
|
||||
{
|
||||
label: 'Panel support snapshot',
|
||||
description: 'Dashboard JSON used to help troubleshoot visualization issues',
|
||||
label: t('dashboard-scene.support-snapshot-service.label.panel-support-snapshot', 'Panel support snapshot'),
|
||||
description: t(
|
||||
'dashboard-scene.support-snapshot-service.description.dashboard-troubleshoot-visualization-issues',
|
||||
'Dashboard JSON used to help troubleshoot visualization issues'
|
||||
),
|
||||
value: ShowMessage.PanelSnapshot,
|
||||
},
|
||||
],
|
||||
@@ -102,7 +106,10 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
|
||||
if (markdownText.length > maxLen) {
|
||||
this.setState({
|
||||
error: {
|
||||
title: 'Copy to clipboard failed',
|
||||
title: t(
|
||||
'dashboard-scene.support-snapshot-service.title.copy-to-clipboard-failed',
|
||||
'Copy to clipboard failed'
|
||||
),
|
||||
message: 'Snapshot is too large, consider download and attaching a file instead',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,6 +149,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
|
||||
}
|
||||
|
||||
if (annotationsCount > 0) {
|
||||
const DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE = 'Annotations';
|
||||
dashboard.panels.push({
|
||||
id: 7,
|
||||
gridPos: {
|
||||
@@ -158,7 +159,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
|
||||
y: 20,
|
||||
},
|
||||
type: 'table',
|
||||
title: 'Annotations',
|
||||
title: DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE,
|
||||
datasource: {
|
||||
type: 'datasource',
|
||||
uid: '-- Dashboard --',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { locationUtil, UrlQueryMap } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { sceneGraph } from '@grafana/scenes';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
@@ -545,7 +546,10 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
status: 404,
|
||||
message: 'Dashboard not found',
|
||||
message: t(
|
||||
'dashboard-scene.dashboard-scene-page-state-manager.message.dashboard-not-found',
|
||||
'Dashboard not found'
|
||||
),
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -728,7 +732,10 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
status: 404,
|
||||
message: 'Dashboard not found',
|
||||
message: t(
|
||||
'dashboard-scene.dashboard-scene-page-state-manager-v2.message.dashboard-not-found',
|
||||
'Dashboard not found'
|
||||
),
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as H from 'history';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { NavIndex, PanelPlugin } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
PanelBuilders,
|
||||
@@ -245,7 +246,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
|
||||
return {
|
||||
text: 'Edit panel',
|
||||
text: t('dashboard-scene.panel-editor.text.edit-panel', 'Edit panel'),
|
||||
parentItem: dashboard.getPageNav(location, navIndex),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,8 +76,14 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
|
||||
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations },
|
||||
{ label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions },
|
||||
{
|
||||
label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.visualizations', 'Visualizations'),
|
||||
value: VisualizationSelectPaneTab.Visualizations,
|
||||
},
|
||||
{
|
||||
label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.suggestions', 'Suggestions'),
|
||||
value: VisualizationSelectPaneTab.Suggestions,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
||||
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
|
||||
@@ -19,7 +20,7 @@ import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescriptor {
|
||||
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||
title: 'Panel options',
|
||||
title: t('dashboard-scene.get-panel-frame-options.descriptor.title.panel-options', 'Panel options'),
|
||||
id: 'Panel options',
|
||||
isOpenDefault: true,
|
||||
});
|
||||
@@ -32,7 +33,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
descriptor
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Title',
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'),
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle() {
|
||||
@@ -49,7 +50,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Description',
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'),
|
||||
value: panel.state.description,
|
||||
render: function renderDescription() {
|
||||
return <PanelDescriptionTextArea panel={panel} />;
|
||||
@@ -64,7 +65,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Transparent background',
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'),
|
||||
render: function renderTransparent() {
|
||||
return <PanelBackgroundSwitch panel={panel} />;
|
||||
},
|
||||
@@ -72,13 +73,13 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
)
|
||||
.addCategory(
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
title: 'Panel links',
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
|
||||
id: 'Panel links',
|
||||
isOpenDefault: false,
|
||||
itemsCount: links?.length,
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Panel links',
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
|
||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { RepositoryView } from 'app/api/clients/provisioning';
|
||||
|
||||
export function getDefaultWorkflow(config?: RepositoryView, loadedFromRef?: string) {
|
||||
@@ -27,7 +28,10 @@ export function getWorkflowOptions(config?: RepositoryView, ref?: string) {
|
||||
case 'write':
|
||||
return { label: ref ? `Push to ${ref}` : 'Save', value };
|
||||
case 'branch':
|
||||
return { label: 'Push to a new branch', value };
|
||||
return {
|
||||
label: t('dashboard-scene.get-workflow-options.label.push-to-a-new-branch', 'Push to a new branch'),
|
||||
value,
|
||||
};
|
||||
}
|
||||
return { label: value, value };
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as H from 'history';
|
||||
|
||||
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config, locationService, RefreshEvent } from '@grafana/runtime';
|
||||
import {
|
||||
sceneGraph,
|
||||
@@ -192,7 +193,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
public constructor(state: Partial<DashboardSceneState>, serializerVersion: 'v1' | 'v2' = 'v1') {
|
||||
super({
|
||||
title: 'Dashboard',
|
||||
title: t('dashboard-scene.dashboard-scene.title.dashboard', 'Dashboard'),
|
||||
meta: {},
|
||||
editable: true,
|
||||
$timeRange: state.$timeRange ?? new SceneTimeRange({}),
|
||||
@@ -321,7 +322,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: 'Discard changes to dashboard?',
|
||||
title: t('dashboard-scene.dashboard-scene.title.discard-changes-to-dashboard', 'Discard changes to dashboard?'),
|
||||
text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`,
|
||||
icon: 'trash-alt',
|
||||
yesText: 'Discard',
|
||||
@@ -462,7 +463,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
if (viewPanelScene) {
|
||||
pageNav = {
|
||||
text: 'View panel',
|
||||
text: t('dashboard-scene.dashboard-scene.text.view-panel', 'View panel'),
|
||||
parentItem: pageNav,
|
||||
url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()),
|
||||
};
|
||||
@@ -470,7 +471,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
if (editPanel) {
|
||||
pageNav = {
|
||||
text: 'Edit panel',
|
||||
text: t('dashboard-scene.dashboard-scene.text.edit-panel', 'Edit panel'),
|
||||
parentItem: pageNav,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { t as internalT } from '@grafana/i18n/internal';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { ConfirmModal, ToolbarButton } from '@grafana/ui';
|
||||
|
||||
@@ -33,7 +34,10 @@ const onOpenSnapshotOriginalDashboard = (originalUrl: string) => {
|
||||
new ShowModalReactEvent({
|
||||
component: ConfirmModal,
|
||||
props: {
|
||||
title: 'Proceed to external site?',
|
||||
title: internalT(
|
||||
'dashboard-scene.on-open-snapshot-original-dashboard.title.proceed-to-external-site',
|
||||
'Proceed to external site?'
|
||||
),
|
||||
modalClass: css({
|
||||
width: 'max-content',
|
||||
maxWidth: '80vw',
|
||||
|
||||
@@ -328,7 +328,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
// Add specific "Metrics drilldown" menu
|
||||
if (metricsDrilldownLinks.length > 0) {
|
||||
items.push({
|
||||
text: 'Metrics drilldown',
|
||||
text: t('dashboard-scene.panel-menu-behavior.async-func.text.metrics-drilldown', 'Metrics drilldown'),
|
||||
iconClassName: 'code-branch',
|
||||
type: 'submenu',
|
||||
subMenu: createExtensionSubMenu(metricsDrilldownLinks),
|
||||
@@ -338,7 +338,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
// Add generic "Extensions" menu for other links
|
||||
if (otherLinks.length > 0) {
|
||||
items.push({
|
||||
text: 'Extensions',
|
||||
text: t('dashboard-scene.panel-menu-behavior.async-func.text.extensions', 'Extensions'),
|
||||
iconClassName: 'plug',
|
||||
type: 'submenu',
|
||||
subMenu: createExtensionSubMenu(otherLinks),
|
||||
@@ -544,8 +544,8 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu
|
||||
export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: 'Remove panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
title: t('dashboard-scene.on-remove-panel.title.remove-panel', 'Remove panel'),
|
||||
text: t('dashboard-scene.on-remove-panel.text.remove-panel', 'Are you sure you want to remove this panel?'),
|
||||
icon: 'trash-alt',
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => dashboard.removePanel(panel),
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
@@ -14,7 +15,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
|
||||
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Column options',
|
||||
title: t('dashboard-scene.get-edit-options.title.column-options', 'Column options'),
|
||||
skipField: true,
|
||||
render: () => <GridLayoutColumns layoutManager={layoutManager} />,
|
||||
})
|
||||
@@ -22,7 +23,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
|
||||
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Row height options',
|
||||
title: t('dashboard-scene.get-edit-options.title.row-height-options', 'Row height options'),
|
||||
skipField: true,
|
||||
render: () => <GridLayoutRows layoutManager={layoutManager} />,
|
||||
})
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ export class DefaultGridLayoutManager
|
||||
|
||||
const row = new SceneGridRow({
|
||||
key: getVizPanelKeyForPanelId(id),
|
||||
title: 'Row title',
|
||||
title: t('dashboard-scene.default-grid-layout-manager.row.title.row-title', 'Row title'),
|
||||
actions: new RowActions({}),
|
||||
y: 0,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { VariableModel, defaultDashboard } from '@grafana/schema';
|
||||
import {
|
||||
@@ -55,7 +56,7 @@ export async function buildNewDashboardSaveModel(urlFolderUid?: string): Promise
|
||||
dashboard: {
|
||||
...defaultDashboard,
|
||||
uid: '',
|
||||
title: 'New dashboard',
|
||||
title: t('dashboard-scene.build-new-dashboard-save-model.data.title.new-dashboard', 'New dashboard'),
|
||||
panels: [],
|
||||
timezone: config.bootData.user?.timezone || defaultDashboard.timezone,
|
||||
},
|
||||
@@ -112,7 +113,7 @@ export async function buildNewDashboardSaveModelV2(
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
spec: {
|
||||
...defaultDashboardV2Spec(),
|
||||
title: 'New dashboard',
|
||||
title: t('dashboard-scene.build-new-dashboard-save-model-v2.data.title.new-dashboard', 'New dashboard'),
|
||||
timeSettings: {
|
||||
...defaultTimeSettingsSpec(),
|
||||
timezone: config.bootData.user?.timezone || defaultTimeSettingsSpec().timezone,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
@@ -119,8 +120,9 @@ interface EditLinkViewProps {
|
||||
}
|
||||
|
||||
function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }: EditLinkViewProps) {
|
||||
const { t } = useTranslate();
|
||||
const editLinkPageNav = {
|
||||
text: 'Edit link',
|
||||
text: t('dashboard-scene.edit-link-view.edit-link-page-nav.text.edit-link', 'Edit link'),
|
||||
parentItem: pageNav,
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi
|
||||
const dashboardPageNav = dashboard.getPageNav(location, navIndex);
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'Settings',
|
||||
text: t('dashboard-scene.use-dashboard-edit-page-nav.page-nav.text.settings', 'Settings'),
|
||||
url: locationUtil.getUrlForPartial(location, { editview: 'settings', editIndex: null }),
|
||||
children: [],
|
||||
parentItem: dashboardPageNav,
|
||||
|
||||
@@ -136,7 +136,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
fill="outline"
|
||||
onClick={() => {
|
||||
showModal(ConfirmModal, {
|
||||
title: 'Delete variable',
|
||||
title: t('dashboard-scene.variable-editor-form.title.delete-variable', 'Delete variable'),
|
||||
body: `Are you sure you want to delete: ${name}?`,
|
||||
confirmText: 'Delete variable',
|
||||
onConfirm: onDeleteVariable(hideModal),
|
||||
|
||||
@@ -59,9 +59,15 @@ export function ResourceExport({
|
||||
<Label>{switchExportModeLabel}</Label>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'Classic', value: ExportMode.Classic },
|
||||
{ label: 'V1 Resource', value: ExportMode.V1Resource },
|
||||
{ label: 'V2 Resource', value: ExportMode.V2Resource },
|
||||
{ label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
||||
value: ExportMode.V1Resource,
|
||||
},
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
|
||||
value: ExportMode.V2Resource,
|
||||
},
|
||||
]}
|
||||
value={exportMode}
|
||||
onChange={(value) => onExportModeChange(value)}
|
||||
@@ -73,8 +79,8 @@ export function ResourceExport({
|
||||
<Label>{switchExportFormatLabel}</Label>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'JSON', value: 'json' },
|
||||
{ label: 'YAML', value: 'yaml' },
|
||||
{ label: t('dashboard-scene.resource-export.label.json', 'JSON'), value: 'json' },
|
||||
{ label: t('dashboard-scene.resource-export.label.yaml', 'YAML'), value: 'yaml' },
|
||||
]}
|
||||
value={isViewingYAML ? 'yaml' : 'json'}
|
||||
onChange={onViewYAML}
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
@@ -24,6 +24,7 @@ interface Props extends SceneComponentProps<SharePublicDashboardTab> {
|
||||
}
|
||||
|
||||
export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: Props) {
|
||||
const { t } = useTranslate();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||
@@ -45,7 +46,7 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }:
|
||||
dashboard.showModal(
|
||||
new ConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Revoke public URL',
|
||||
title: t('dashboard-scene.config-public-dashboard.title.revoke-public-url', 'Revoke public URL'),
|
||||
icon: 'trash-alt',
|
||||
confirmText: 'Revoke public URL',
|
||||
body: (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
|
||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||
@@ -72,7 +73,12 @@ export function transformUsagesToNetwork(
|
||||
const { variable, tree } = usage;
|
||||
const result: UsagesToNetwork = {
|
||||
variable,
|
||||
nodes: [{ id: 'dashboard', label: 'dashboard' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: t('dashboard-scene.transform-usages-to-network.result.label.dashboard', 'dashboard'),
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
showGraph: false,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppEvents, UrlQueryMap } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { FetchError, getBackendSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -38,7 +39,9 @@ export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard>
|
||||
const fetchError: FetchError = {
|
||||
status: 404,
|
||||
config: { url: `/api/dashboards/uid/${uid}` },
|
||||
data: { message: 'Dashboard not found' },
|
||||
data: {
|
||||
message: t('dashboard.legacy-dashboard-api.fetch-error.message.dashboard-not-found', 'Dashboard not found'),
|
||||
},
|
||||
};
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
@@ -103,7 +104,7 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
|
||||
return this.client.delete(uid, showSuccessAlert).then((v) => ({
|
||||
id: 0,
|
||||
message: v.message,
|
||||
title: 'deleted',
|
||||
title: t('dashboard.k8s-dashboard-api.title.deleted', 'deleted'),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
@@ -91,7 +92,7 @@ export class K8sDashboardV2API
|
||||
return this.client.delete(uid, showSuccessAlert).then((v) => ({
|
||||
id: 0,
|
||||
message: v.message,
|
||||
title: 'deleted',
|
||||
title: t('dashboard.k8s-dashboard-v2api.title.deleted', 'deleted'),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,10 @@ export const DashNav = memo<Props>((props) => {
|
||||
new ShowModalReactEvent({
|
||||
component: ConfirmModal,
|
||||
props: {
|
||||
title: 'Proceed to external site?',
|
||||
title: t(
|
||||
'dashboard.dash-nav.on-open-snapshot-original.title.proceed-to-external-site',
|
||||
'Proceed to external site?'
|
||||
),
|
||||
modalClass: modalStyles,
|
||||
body: (
|
||||
<>
|
||||
|
||||
@@ -85,7 +85,7 @@ export class UnthemedDashboardRow extends Component<DashboardRowProps> {
|
||||
onDelete = () => {
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: 'Delete row',
|
||||
title: t('dashboard.unthemed-dashboard-row.title.delete-row', 'Delete row'),
|
||||
text: 'Are you sure you want to remove this row and all its panels?',
|
||||
altActionText: 'Delete row only',
|
||||
icon: 'trash-alt',
|
||||
|
||||
@@ -116,8 +116,8 @@ export function GeneralSettingsUnconnected({
|
||||
};
|
||||
|
||||
const editableOptions = [
|
||||
{ label: 'Editable', value: true },
|
||||
{ label: 'Read-only', value: false },
|
||||
{ label: t('dashboard.general-settings-unconnected.editable-options.label.editable', 'Editable'), value: true },
|
||||
{ label: t('dashboard.general-settings-unconnected.editable-options.label.readonly', 'Read-only'), value: false },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user