Internationalisation: Mark up azure monitor plugin (#105262)

* start marking up azuremonitor

* more markup

* finish mark up

* add package and correct imports

* convert to functional component + use correct t import

* fix duplicate key + typo

* add extract config + fake french translations to test

* run prettier and fix unit tests

* use nx

* enable i18n lint rules for azure monitor

* remove fake french translations

* await initPluginTranslations

* top level await breaks unit tests

* leave as class component for now
pull/104680/merge
Ashley Harrison 4 days ago committed by GitHub
parent d7715c4220
commit aa0842a1e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Makefile
  2. 14
      eslint.config.js
  3. 1
      package.json
  4. 15
      public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx
  5. 39
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx
  6. 11
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx
  7. 34
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx
  8. 8
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx
  9. 66
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx
  10. 8
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx
  11. 17
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregateItem.tsx
  12. 9
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregationSection.tsx
  13. 14
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterItem.tsx
  14. 17
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterSection.tsx
  15. 14
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FuzzySearch.tsx
  16. 11
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupByItem.tsx
  17. 11
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx
  18. 8
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/KQLPreview.tsx
  19. 13
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LimitSection.tsx
  20. 10
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx
  21. 17
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx
  22. 14
      public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/TableSection.tsx
  23. 30
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AdvancedResourcePicker.tsx
  24. 42
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx
  25. 9
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheetModal.tsx
  26. 17
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx
  27. 5
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx
  28. 4
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/RawQuery.tsx
  29. 28
      public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx
  30. 48
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx
  31. 4
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx
  32. 6
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.test.tsx
  33. 22
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx
  34. 6
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx
  35. 7
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx
  36. 4
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx
  37. 4
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx
  38. 4
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx
  39. 68
      public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx
  40. 14
      public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx
  41. 12
      public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx
  42. 4
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx
  43. 7
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/NestedRow.tsx
  44. 8
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx
  45. 43
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx
  46. 6
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/Search.tsx
  47. 14
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx
  48. 4
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx
  49. 6
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx
  50. 4
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx
  51. 15
      public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx
  52. 72
      public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx
  53. 82
      public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx
  54. 7
      public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx
  55. 289
      public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json
  56. 12
      public/app/plugins/datasource/azuremonitor/locales/i18next-parser.config.cjs
  57. 3
      public/app/plugins/datasource/azuremonitor/module.ts
  58. 5
      public/app/plugins/datasource/azuremonitor/package.json
  59. 23
      public/app/plugins/datasource/azuremonitor/plugin.json
  60. 2
      public/app/plugins/datasource/azuremonitor/webpack.config.ts
  61. 6
      public/locales/en-US/grafana.json
  62. 1
      public/locales/i18next-parser.config.cjs
  63. 4
      yarn.lock

@ -139,6 +139,8 @@ endif
i18n-extract: i18n-extract-enterprise
@echo "Extracting i18n strings for OSS"
yarn run i18next --config public/locales/i18next-parser.config.cjs
@echo "Extracting i18n strings for plugins"
yarn run plugin:i18n-extract
##@ Building
.PHONY: gen-cue

@ -19,6 +19,7 @@ const getEnvConfig = require('./scripts/webpack/env-util');
const envConfig = getEnvConfig();
const enableBettererRules = envConfig.frontend_dev_betterer_eslint_rules;
const pluginsToTranslate = ['public/app/plugins/datasource/azuremonitor'];
/**
* @type {Array<import('eslint').Linter.Config>}
@ -287,15 +288,12 @@ module.exports = [
plugins: {
'@grafana': grafanaPlugin,
},
files: ['public/**/*.{ts,tsx,js,jsx}', 'packages/grafana-ui/**/*.{ts,tsx,js,jsx}'],
ignores: [
'public/app/plugins/**',
'**/*.story.tsx',
'**/*.{test,spec}.{ts,tsx}',
'**/__mocks__/',
'public/test',
'**/spec/**/*.{ts,tsx}',
files: [
'public/app/!(plugins)/**/*.{ts,tsx,js,jsx}',
'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}'],
rules: {
'@grafana/no-untranslated-strings': 'error',
'@grafana/no-translation-top-level': 'error',

@ -65,6 +65,7 @@
"plugin:build": "nx run-many -t build --projects='tag:scope:plugin'",
"plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'",
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
"plugin:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:plugin'",
"process-specs": "node --experimental-strip-types scripts/process-specs.ts",
"generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts",
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types' plop --plopfile public/app/api/generator/plopfile.ts"

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { FieldValidationMessage, MultiSelect } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -17,6 +18,7 @@ const SubscriptionField = ({ query, subscriptions, variableOptionGroup, onQueryC
const [error, setError] = useState<boolean>(false);
const [values, setValues] = useState<Array<SelectableValue<string>>>([]);
const options = useMemo(() => [...subscriptions, variableOptionGroup], [subscriptions, variableOptionGroup]);
const { t } = useTranslate();
useEffect(() => {
if (query.subscriptions && query.subscriptions.length > 0) {
@ -54,7 +56,10 @@ const SubscriptionField = ({ query, subscriptions, variableOptionGroup, onQueryC
};
return (
<Field label="Subscriptions" data-testid={selectors.components.queryEditor.argsQueryEditor.subscriptions.input}>
<Field
label={t('components.subscription-field.label-subscriptions', 'Subscriptions')}
data-testid={selectors.components.queryEditor.argsQueryEditor.subscriptions.input}
>
<>
<MultiSelect
isClearable
@ -64,7 +69,13 @@ const SubscriptionField = ({ query, subscriptions, variableOptionGroup, onQueryC
options={options}
width={38}
/>
{error ? <FieldValidationMessage>At least one subscription must be chosen.</FieldValidationMessage> : null}
{error ? (
<FieldValidationMessage>
<Trans i18nKey="components.subscription-field.validation-subscriptions">
At least one subscription must be chosen.
</Trans>
</FieldValidationMessage>
) : null}
</>
</Field>
);

@ -2,6 +2,7 @@ import { ChangeEvent } from 'react';
import { AzureClientSecretCredentials, AzureCredentials } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Field, Select, Input, Button } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -15,6 +16,7 @@ export interface AppRegistrationCredentialsProps {
export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProps) => {
const { azureCloudOptions, disabled, credentials, onCredentialsChange } = props;
const { t } = useTranslate();
const onAzureCloudChange = (selected: SelectableValue<string>) => {
const updated: AzureCredentials = {
@ -60,14 +62,14 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
<>
{azureCloudOptions && (
<Field
label="Azure Cloud"
label={t('components.app-registration-credentials.label-azure-cloud', 'Azure Cloud')}
data-testid={selectors.components.configEditor.azureCloud.input}
htmlFor="azure-cloud-type"
disabled={disabled}
>
<Select
inputId="azure-cloud-type"
aria-label="Azure Cloud"
aria-label={t('components.app-registration-credentials.aria-label-azure-cloud', 'Azure Cloud')}
className="width-15"
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions}
@ -76,7 +78,7 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
</Field>
)}
<Field
label="Directory (tenant) ID"
label={t('components.app-registration-credentials.label-tenant-id', 'Directory (tenant) ID')}
required={credentials.authType === 'clientsecret'}
data-testid={selectors.components.configEditor.tenantID.input}
htmlFor="tenant-id"
@ -84,8 +86,9 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
error={'Tenant ID is required'}
>
<Input
aria-label="Tenant ID"
aria-label={t('components.app-registration-credentials.aria-label-tenant-id', 'Tenant ID')}
className="width-30"
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={onTenantIdChange}
@ -93,7 +96,7 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
/>
</Field>
<Field
label="Application (client) ID"
label={t('components.app-registration-credentials.label-client-id', 'Application (client) ID')}
required={credentials.authType === 'clientsecret'}
data-testid={selectors.components.configEditor.clientID.input}
htmlFor="client-id"
@ -102,7 +105,8 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
>
<Input
className="width-30"
aria-label="Client ID"
aria-label={t('components.app-registration-credentials.aria-label-client-id', 'Client ID')}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={onClientIdChange}
@ -111,22 +115,32 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
</Field>
{!disabled &&
(typeof credentials.clientSecret === 'symbol' ? (
<Field label="Client Secret" htmlFor="client-secret" required>
<Field
label={t('components.app-registration-credentials.label-symbol-client-secret', 'Client Secret')}
htmlFor="client-secret"
required
>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Input
aria-label="Client Secret"
placeholder="configured"
aria-label={t(
'components.app-registration-credentials.aria-label-symbol-client-secret',
'Client Secret'
)}
placeholder={t(
'components.app-registration-credentials.placeholder-symbol-client-secret',
'configured'
)}
disabled={true}
data-testid={'client-secret'}
/>
<Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
Reset
<Trans i18nKey="components.app-registration-credentials.reset-symbol-client-secret">Reset</Trans>
</Button>
</div>
</Field>
) : (
<Field
label="Client Secret"
label={t('components.app-registration-credentials.label-client-secret', 'Client Secret')}
data-testid={selectors.components.configEditor.clientSecret.input}
required
htmlFor="client-secret"
@ -135,7 +149,8 @@ export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProp
>
<Input
className="width-30"
aria-label="Client Secret"
aria-label={t('components.app-registration-credentials.aria-label-client-secret', 'Client Secret')}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''}
onChange={onClientSecretChange}

@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { AzureAuthType, AzureCredentials, getAzureClouds } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ConfigSection } from '@grafana/plugin-ui';
import { Select, Field } from '@grafana/ui';
@ -39,6 +40,7 @@ export const AzureCredentialsForm = (props: Props) => {
workloadIdentityEnabled,
userIdentityEnabled,
} = props;
const { t } = useTranslate();
const authTypeOptions = useMemo(() => {
let opts: Array<SelectableValue<AzureAuthType>> = [
@ -98,11 +100,14 @@ export const AzureCredentialsForm = (props: Props) => {
};
return (
<ConfigSection title="Authentication">
<ConfigSection title={t('components.azure-credentials-form.title-authentication', 'Authentication')}>
{authTypeOptions.length > 1 && (
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
label={t('components.azure-credentials-form.label-authentication', 'Authentication')}
description={t(
'components.azure-credentials-form.description-authentication',
'Choose the type of authentication to Azure services'
)}
data-testid={selectors.components.configEditor.authType.select}
htmlFor="authentication-type"
>

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import * as React from 'react';
import { Field, Switch, useTheme2 } from '@grafana/ui';
import { Trans, useTranslate } from '@grafana/i18n';
import { Field, Switch, TextLink, useTheme2 } from '@grafana/ui';
import { AzureMonitorDataSourceJsonData } from '../../types';
@ -12,6 +13,7 @@ export interface Props {
export const BasicLogsToggle = (props: Props) => {
const { options, onBasicLogsEnabledChange } = props;
const { t } = useTranslate();
const theme = useTheme2();
const styles = {
@ -31,21 +33,29 @@ export const BasicLogsToggle = (props: Props) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => onBasicLogsEnabledChange(e.target.checked);
const description = (
<p className={styles.text}>
Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for{' '}
<a
href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1"
target="__blank"
rel="noreferrer"
>
Basic Logs
</a>
.
<Trans i18nKey="components.basic-logs-toggle.description-basic-logs">
Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for{' '}
<TextLink
href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1"
external
>
Basic Logs
</TextLink>
.
</Trans>
</p>
);
return (
<Field description={description} label="Enable Basic Logs">
<Field
description={description}
label={t('components.basic-logs-toggle.label-enable-basic-logs', 'Enable Basic Logs')}
>
<div>
<Switch aria-label="Basic Logs" onChange={onChange} value={options.basicLogsEnabled ?? false} />
<Switch
aria-label={t('components.basic-logs-toggle.aria-label-enable-basic-logs', 'Basic Logs')}
onChange={onChange}
value={options.basicLogsEnabled ?? false}
/>
</div>
</Field>
);

@ -1,6 +1,7 @@
import { PureComponent } from 'react';
import { DataSourcePluginOptionsEditorProps, SelectableValue, updateDatasourcePluginOption } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui';
import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv, config } from '@grafana/runtime';
import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui';
@ -118,8 +119,11 @@ export class ConfigEditor extends PureComponent<Props, State> {
<>
<Divider />
<ConfigSection
title="Additional settings"
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy."
title={t('components.config-editor.title-additional-settings', 'Additional settings')}
description={t(
'components.config-editor.description-additional-settings',
'Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy.'
)}
isCollapsible={true}
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
>

@ -2,9 +2,10 @@ import { useMemo } from 'react';
import { AadCurrentUserCredentials, AzureCredentials, instanceOfAzureCredential } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { ConfigSection } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { Select, Field, RadioButtonGroup, Alert, Stack } from '@grafana/ui';
import { Select, Field, RadioButtonGroup, Alert, Stack, TextLink } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -29,6 +30,7 @@ export const CurrentUserFallbackCredentials = (props: Props) => {
managedIdentityEnabled,
workloadIdentityEnabled,
} = props;
const { t } = useTranslate();
type FallbackCredentialAuthTypeOptions = 'clientsecret' | 'msi' | 'workloadidentity';
const authTypeOptions = useMemo(() => {
@ -90,45 +92,66 @@ export const CurrentUserFallbackCredentials = (props: Props) => {
if (!config.azure.userIdentityFallbackCredentialsEnabled) {
return (
<Alert severity="info" title="Fallback Credentials Disabled">
<>
<Alert
severity="info"
title={t(
'components.current-user-fallback-credentials.title-fallback-credentials-disabled',
'Fallback Credentials Disabled'
)}
>
<Trans i18nKey="components.current-user-fallback-credentials.alert-fallback-credentials-disabled">
Fallback credentials have been disabled. As user-based authentication only inherently supports requests with a
user in scope, features such as alerting, recorded queries, or reporting will not function as expected. Please
review the{' '}
<a
<TextLink
href="https://grafana.com/docs/grafana/latest/datasources/azuremonitor/deprecated-application-insights/"
target="_blank"
rel="noreferrer"
external
>
documentation
</a>{' '}
</TextLink>{' '}
for more details.
</>
</Trans>
</Alert>
);
}
return (
<ConfigSection title="Fallback Service Credentials" isCollapsible={true}>
<Alert severity="info" title="Service Credentials">
<ConfigSection
title={t(
'components.current-user-fallback-credentials.title-fallback-service-credentials',
'Fallback Service Credentials'
)}
isCollapsible={true}
>
<Alert
severity="info"
title={t('components.current-user-fallback-credentials.title-service-credentials', 'Service Credentials')}
>
<Stack direction={'column'}>
<div>
User-based authentication does not inherently support Grafana features that make requests to the data source
without a users details available to the request. An example of this is alerting. If you wish to ensure that
features that do not have a user in the context of the request still function, please provide fallback
credentials below.
<Trans i18nKey="components.current-user-fallback-credentials.body-service-credentials">
User-based authentication does not inherently support Grafana features that make requests to the data
source without a users details available to the request. An example of this is alerting. If you wish to
ensure that features that do not have a user in the context of the request still function, please provide
fallback credentials below.
</Trans>
</div>
<div>
<b>
Note: Features like alerting will be restricted to the access level of the fallback credentials rather
than the user. This may present confusion for users and should be clarified.
<Trans i18nKey="components.current-user-fallback-credentials.note-service-credentials">
Note: Features like alerting will be restricted to the access level of the fallback credentials rather
than the user. This may present confusion for users and should be clarified.
</Trans>
</b>
</div>
</Stack>
</Alert>
<Field
label="Service Credentials"
description="Choose if fallback service credentials are enabled or disabled for this data source"
label={t('components.current-user-fallback-credentials.label-service-credentials', 'Service Credentials')}
description={t(
'components.current-user-fallback-credentials.description-service-credentials',
'Choose if fallback service credentials are enabled or disabled for this data source'
)}
data-testid={selectors.components.configEditor.serviceCredentialsEnabled.button}
>
<RadioButtonGroup
@ -145,8 +168,11 @@ export const CurrentUserFallbackCredentials = (props: Props) => {
<>
{authTypeOptions.length > 0 && (
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
label={t('components.current-user-fallback-credentials.label-authentication', 'Authentication')}
description={t(
'components.current-user-fallback-credentials.description-authentication',
'Choose the type of authentication to Azure services'
)}
data-testid={selectors.components.configEditor.authType.select}
htmlFor="authentication-type"
>

@ -2,6 +2,7 @@ import { useEffect, useReducer } from 'react';
import { AzureCredentials, isCredentialsComplete } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Select, Button, Field } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -29,6 +30,7 @@ export const DefaultSubscription = (props: Props) => {
} = props;
const hasRequiredFields = isCredentialsComplete(credentials);
const [loadSubscriptionsClicked, onLoadSubscriptions] = useReducer((val) => val + 1, 0);
const { t } = useTranslate();
useEffect(() => {
if (!getSubscriptions || !hasRequiredFields) {
@ -69,14 +71,14 @@ export const DefaultSubscription = (props: Props) => {
return (
<>
<Field
label="Default Subscription"
label={t('components.default-subscription.label-default-subscription', 'Default Subscription')}
data-testid={selectors.components.configEditor.defaultSubscription.input}
htmlFor="default-subscription"
>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Select
inputId="default-subscription"
aria-label="Default Subscription"
aria-label={t('components.default-subscription.aria-label-default-subscription', 'Default Subscription')}
value={
options.subscriptionId ? subscriptions.find((opt) => opt.value === options.subscriptionId) : undefined
}
@ -91,7 +93,7 @@ export const DefaultSubscription = (props: Props) => {
disabled={!hasRequiredFields || disabled}
data-testid={selectors.components.configEditor.loadSubscriptions.button}
>
Load Subscriptions
<Trans i18nKey="components.default-subscription.load-subscriptions">Load Subscriptions</Trans>
</Button>
</div>
</Field>

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { InputGroup, AccessoryButton } from '@grafana/plugin-ui';
import { Select, Label, Input } from '@grafana/ui';
@ -27,6 +28,7 @@ const AggregateItem: React.FC<AggregateItemProps> = ({
columns,
templateVariableOptions,
}) => {
const { t } = useTranslate();
const isPercentile = aggregate.reduce?.name === 'percentile';
const isCountAggregate = aggregate.reduce?.name?.includes('count');
@ -103,7 +105,7 @@ const AggregateItem: React.FC<AggregateItemProps> = ({
return (
<InputGroup>
<Select
aria-label="aggregate function"
aria-label={t('components.aggregate-item.aria-label-aggregate-function', 'Aggregate function')}
width={inputFieldSize}
value={aggregate.reduce?.name ? { label: aggregate.reduce.name, value: aggregate.reduce.name } : null}
options={aggregateOptions}
@ -126,7 +128,9 @@ const AggregateItem: React.FC<AggregateItemProps> = ({
}
}}
/>
<Label style={{ margin: '9px 9px 0 9px' }}>OF</Label>
<Label style={{ margin: '9px 9px 0 9px' }}>
<Trans i18nKey="components.aggregate-item.label-percentile">OF</Trans>
</Label>
</>
) : (
<></>
@ -134,7 +138,7 @@ const AggregateItem: React.FC<AggregateItemProps> = ({
{!isCountAggregate ? (
<Select
aria-label="column"
aria-label={t('components.aggregate-item.aria-label-column', 'Column')}
width={inputFieldSize}
value={columnValue ? { label: columnValue, value: columnValue } : null}
options={selectableOptions}
@ -144,7 +148,12 @@ const AggregateItem: React.FC<AggregateItemProps> = ({
<></>
)}
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
<AccessoryButton
aria-label={t('components.aggregate-item.aria-label-remove', 'Remove')}
icon="times"
variant="secondary"
onClick={onDelete}
/>
</InputGroup>
);
};

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorList, EditorRow } from '@grafana/plugin-ui';
import { BuilderQueryEditorReduceExpression } from '../../dataquery.gen';
@ -21,6 +22,7 @@ export const AggregateSection: React.FC<AggregateSectionProps> = ({
buildAndUpdateQuery,
templateVariableOptions,
}) => {
const { t } = useTranslate();
const builderQuery = query.azureLogAnalytics?.builderQuery;
const [aggregates, setAggregates] = useState<BuilderQueryEditorReduceExpression[]>(
builderQuery?.reduce?.expressions || []
@ -70,9 +72,12 @@ export const AggregateSection: React.FC<AggregateSectionProps> = ({
<EditorRow>
<EditorFieldGroup>
<EditorField
label="Aggregate"
label={t('components.aggregate-section.label-aggregate', 'Aggregate')}
optional={true}
tooltip={`Perform calculations across rows of data, such as count, sum, average, minimum, maximum, standard deviation or percentiles.`}
tooltip={t(
'components.aggregate-section.tooltip-aggregate',
'Perform calculations across rows of data, such as count, sum, average, minimum, maximum, standard deviation or percentiles.'
)}
>
<EditorList
items={aggregates}

@ -1,6 +1,7 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Button, Combobox, ComboboxOption, Label, Select } from '@grafana/ui';
import { BuilderQueryEditorWhereExpressionItems } from '../../dataquery.gen';
@ -33,24 +34,25 @@ export const FilterItem: React.FC<FilterItemProps> = ({
getFilterValues,
showOr,
}) => {
const { t } = useTranslate();
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Select
aria-label="column"
aria-label={t('components.filter-item.aria-label-column', 'Column')}
width={inputFieldSize}
value={valueToDefinition(filter.property.name)}
options={selectableOptions.filter((opt) => !usedColumns.includes(opt.value!))}
onChange={(e) => e.value && onChange(groupIndex, 'property', e.value, filterIndex)}
/>
<Select
aria-label="operator"
aria-label={t('components.filter-item.aria-label-operator', 'Operator')}
width={12}
value={{ label: filter.operator.name, value: filter.operator.name }}
options={toOperatorOptions('string')}
onChange={(e) => e.value && onChange(groupIndex, 'operator', e.value, filterIndex)}
/>
<Combobox
aria-label="column value"
aria-label={t('components.filter-item.aria-label-column-value', 'Column value')}
value={
filter.operator.value
? {
@ -65,7 +67,11 @@ export const FilterItem: React.FC<FilterItemProps> = ({
disabled={!filter.property?.name}
/>
<Button variant="secondary" icon="times" onClick={() => onDelete(groupIndex, filterIndex)} />
{showOr && <Label style={{ padding: '9px 14px' }}>OR</Label>}
{showOr && (
<Label style={{ padding: '9px 14px' }}>
<Trans i18nKey="components.filter-item.label-or">OR</Trans>
</Label>
)}
</div>
);
};

@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import { CoreApp, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui';
import { Button, ComboboxOption, Label, useStyles2 } from '@grafana/ui';
@ -42,6 +43,7 @@ export const FilterSection: React.FC<FilterSectionProps> = ({
datasource,
timeRange,
}) => {
const { t } = useTranslate();
const styles = useStyles2(() => ({ filters: css({ marginBottom: '8px' }) }));
const builderQuery = query.azureLogAnalytics?.builderQuery;
@ -206,7 +208,14 @@ export const FilterSection: React.FC<FilterSectionProps> = ({
return (
<EditorRow>
<EditorFieldGroup>
<EditorField label="Filters" optional tooltip="Narrow results by applying conditions to specific columns.">
<EditorField
label={t('components.filter-section.label-filters', 'Filters')}
optional
tooltip={t(
'components.filter-section.tooltip-filters',
'Narrow results by applying conditions to specific columns.'
)}
>
<div className={styles.filters}>
{filters.length === 0 || filters.every((g) => g.expressions.length === 0) ? (
<InputGroup>
@ -217,7 +226,9 @@ export const FilterSection: React.FC<FilterSectionProps> = ({
{filters.map((group, groupIndex) => (
<div key={groupIndex}>
{groupIndex > 0 && filters[groupIndex - 1]?.expressions.length > 0 && (
<Label style={{ padding: '9px 14px' }}>AND</Label>
<Label style={{ padding: '9px 14px' }}>
<Trans i18nKey="components.filter-section.label-and">AND</Trans>
</Label>
)}
<InputGroup>
<>
@ -247,7 +258,7 @@ export const FilterSection: React.FC<FilterSectionProps> = ({
))}
{filters.some((g) => g.expressions.length > 0) && (
<Button variant="secondary" onClick={onAddAndFilters} style={{ marginTop: '8px' }}>
Add group
<Trans i18nKey="components.filter-section.label-add-group">Add group</Trans>
</Button>
)}
</>

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorRow, EditorFieldGroup, EditorField, InputGroup } from '@grafana/plugin-ui';
import { Button, Input, Select } from '@grafana/ui';
@ -26,6 +27,7 @@ export const FuzzySearch: React.FC<FuzzySearchProps> = ({
allColumns,
templateVariableOptions,
}) => {
const { t } = useTranslate();
const builderQuery = query.azureLogAnalytics?.builderQuery;
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
@ -100,10 +102,12 @@ export const FuzzySearch: React.FC<FuzzySearchProps> = ({
<EditorRow>
<EditorFieldGroup>
<EditorField
label="Fuzzy Search"
label={t('components.fuzzy-search.label-fuzzy-search', 'Fuzzy Search')}
optional={true}
tooltip={`Find approximate text matches with tolerance for spelling variations. By default, fuzzy search scans all
columns (*) in the entire table, not just specific fields.`}
tooltip={t(
'components.fuzzy-search.tooltip-fuzzy-search',
'Find approximate text matches with tolerance for spelling variations. By default, fuzzy search scans all columns (*) in the entire table, not just specific fields.'
)}
>
<InputGroup>
{isOpen ? (
@ -111,12 +115,12 @@ export const FuzzySearch: React.FC<FuzzySearchProps> = ({
<Input
className="width-10"
type="text"
placeholder="Enter search term"
placeholder={t('components.fuzzy-search.placeholder-search-team', 'Enter search term')}
value={searchTerm}
onChange={(e) => updateFuzzySearch(selectedColumn, e.currentTarget.value)}
/>
<Select
aria-label="Select Column"
aria-label={t('components.fuzzy-search.aria-label-select-column', 'Select Column')}
options={selectableOptions}
value={{ label: selectedColumn || '*', value: selectedColumn || '*' }}
onChange={(e: SelectableValue<string>) => updateFuzzySearch(e.value ?? '*', searchTerm)}

@ -1,6 +1,7 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { AccessoryButton, InputGroup } from '@grafana/plugin-ui';
import { Select } from '@grafana/ui';
@ -27,6 +28,7 @@ export const GroupByItem: React.FC<GroupByItemProps> = ({
columns,
templateVariableOptions,
}) => {
const { t } = useTranslate();
const columnOptions: Array<SelectableValue<string>> =
columns.length > 0
? columns.map((c) => ({ label: c.label, value: c.value }))
@ -65,14 +67,19 @@ export const GroupByItem: React.FC<GroupByItemProps> = ({
return (
<InputGroup>
<Select
aria-label="column"
aria-label={t('components.group-by-item.aria-label-column', 'Column')}
width={inputFieldSize}
value={groupBy.property?.name ? { label: groupBy.property.name, value: groupBy.property.name } : null}
options={selectableOptions}
allowCustomValue
onChange={handleChange}
/>
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
<AccessoryButton
aria-label={t('components.group-by-item.aria-label-remove', 'Remove')}
icon="times"
variant="secondary"
onClick={onDelete}
/>
</InputGroup>
);
};

@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorList, EditorRow, InputGroup } from '@grafana/plugin-ui';
import { Button } from '@grafana/ui';
@ -27,6 +28,7 @@ export const GroupBySection: React.FC<GroupBySectionProps> = ({
allColumns,
templateVariableOptions,
}) => {
const { t } = useTranslate();
const builderQuery = query.azureLogAnalytics?.builderQuery;
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
const [groupBys, setGroupBys] = useState<BuilderQueryEditorGroupByExpression[]>(
@ -88,11 +90,12 @@ export const GroupBySection: React.FC<GroupBySectionProps> = ({
<EditorRow>
<EditorFieldGroup>
<EditorField
label="Group by"
label={t('components.group-by-section.label-group-by', 'Group by')}
optional={true}
tooltip={`Organize results into categories based on specified columns. Group by can be used independently to list
unique values in selected columns, or combined with aggregate functions to produce summary statistics for
each group. When used alone, it returns distinct combinations of the specified columns.`}
tooltip={t(
'components.group-by-section.tooltip-group-by',
'Organize results into categories based on specified columns. Group by can be used independently to list unique values in selected columns, or combined with aggregate functions to produce summary statistics for each group. When used alone, it returns distinct combinations of the specified columns.'
)}
>
<InputGroup>
{groupBys.length > 0 ? (

@ -3,6 +3,7 @@ import Prism from 'prismjs';
import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui';
import { Button, useStyles2 } from '@grafana/ui';
@ -17,6 +18,7 @@ interface KQLPreviewProps {
const KQLPreview: React.FC<KQLPreviewProps> = ({ query, hidden, setHidden }) => {
const styles = useStyles2(getStyles);
const { t } = useTranslate();
useEffect(() => {
Prism.highlightAll();
@ -25,10 +27,10 @@ const KQLPreview: React.FC<KQLPreviewProps> = ({ query, hidden, setHidden }) =>
return (
<EditorRow>
<EditorFieldGroup>
<EditorField label="Query Preview">
<EditorField label={t('components.kql-preview.label-query-preview', 'Query Preview')}>
<>
<Button hidden={!hidden} variant="secondary" onClick={() => setHidden(false)} size="sm">
show
<Trans i18nKey="components.kql-preview.button-show">Show</Trans>
</Button>
<div className={styles.codeBlock} hidden={hidden}>
<pre className={styles.code}>
@ -36,7 +38,7 @@ const KQLPreview: React.FC<KQLPreviewProps> = ({ query, hidden, setHidden }) =>
</pre>
</div>
<Button hidden={hidden} variant="secondary" onClick={() => setHidden(true)} size="sm">
hide
<Trans i18nKey="components.kql-preview.button-hide">Hide</Trans>
</Button>
</>
</EditorField>

@ -1,5 +1,6 @@
import { useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { EditorRow, EditorFieldGroup, EditorField } from '@grafana/plugin-ui';
import { Input } from '@grafana/ui';
@ -11,6 +12,7 @@ interface LimitSectionProps {
export const LimitSection: React.FC<LimitSectionProps> = (props) => {
const { buildAndUpdateQuery } = props;
const { t } = useTranslate();
const [limit, setLimit] = useState<number>(1000);
const handleQueryLimitUpdate = (newLimit: number) => {
@ -22,11 +24,18 @@ export const LimitSection: React.FC<LimitSectionProps> = (props) => {
return (
<EditorRow>
<EditorFieldGroup>
<EditorField label="Limit" optional={true} tooltip={`Restrict the number of rows returned (default is 1000).`}>
<EditorField
label={t('components.limit-section.label-limit', 'Limit')}
optional={true}
tooltip={t(
'components.limit-section.tooltip-limit',
'Restrict the number of rows returned (default is 1000).'
)}
>
<Input
className="width-5"
type="number"
placeholder="Enter limit"
placeholder={t('components.limit-section.placeholder-limit', 'Enter limit')}
value={limit}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value.replace(/[^0-9]/g, '');

@ -1,6 +1,7 @@
import React, { useMemo, useState, useCallback } from 'react';
import { SelectableValue, TimeRange } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorRows } from '@grafana/plugin-ui';
import { Alert } from '@grafana/ui';
@ -48,6 +49,7 @@ interface LogsQueryBuilderProps {
export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
const { query, onQueryChange, schema, datasource, timeRange, isLoadingSchema } = props;
const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState<boolean>(true);
const { t } = useTranslate();
const tables: AzureLogAnalyticsMetadataTable[] = useMemo(() => {
return schema?.database?.tables || [];
@ -136,7 +138,13 @@ export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
<span data-testid={selectors.components.queryEditor.logsQueryEditor.container.input}>
<EditorRows>
{schema && tables.length === 0 && (
<Alert severity="warning" title="Resource loaded successfully but without any tables" />
<Alert
severity="warning"
title={t(
'components.logs-query-builder.title-no-tables',
'Resource loaded successfully but without any tables'
)}
/>
)}
<TableSection
{...props}

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui';
import { Button, Select, Label } from '@grafana/ui';
@ -24,6 +25,7 @@ export const OrderBySection: React.FC<OrderBySectionProps> = ({ query, allColumn
const builderQuery = query.azureLogAnalytics?.builderQuery;
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
const hasLoadedOrderBy = useRef(false);
const { t } = useTranslate();
const [orderBy, setOrderBy] = useState<BuilderQueryEditorOrderByExpression[]>(
builderQuery?.orderBy?.expressions || []
@ -109,24 +111,29 @@ export const OrderBySection: React.FC<OrderBySectionProps> = ({ query, allColumn
<EditorRow>
<EditorFieldGroup>
<EditorField
label="Order By"
label={t('components.order-by-section.label-order-by', 'Order By')}
optional={true}
tooltip={`Sort results based on one or more columns in ascending or descending order.`}
tooltip={t(
'components.order-by-section.tooltip-order-by',
'Sort results based on one or more columns in ascending or descending order.'
)}
>
<>
{orderBy.length > 0 ? (
orderBy.map((entry, index) => (
<InputGroup key={index}>
<Select
aria-label="Order by column"
aria-label={t('components.order-by-section.aria-label-order-by-column', 'Order by column')}
width={inputFieldSize}
value={entry.property?.name ? { label: entry.property.name, value: entry.property.name } : null}
options={columnOptions}
onChange={(e) => e.value && handleOrderByChange(index, 'column', e.value)}
/>
<Label style={{ margin: '9px 9px 0 9px' }}>BY</Label>
<Label style={{ margin: '9px 9px 0 9px' }}>
<Trans i18nKey="components.order-by-section.label-by">BY</Trans>
</Label>
<Select
aria-label="Order Direction"
aria-label={t('components.order-by-section.aria-label-order-direction', 'Order Direction')}
width={inputFieldSize}
value={orderOptions.find((o) => o.value === entry.order) || null}
options={orderOptions}

@ -1,6 +1,7 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui';
import { Button, Select } from '@grafana/ui';
@ -26,6 +27,7 @@ interface TableSectionProps {
export const TableSection: React.FC<TableSectionProps> = (props) => {
const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions, isLoadingSchema } = props;
const { t } = useTranslate();
const builderQuery = query.azureLogAnalytics?.builderQuery;
const selectedColumns = query.azureLogAnalytics?.builderQuery?.columns?.columns || [];
@ -139,27 +141,27 @@ export const TableSection: React.FC<TableSectionProps> = (props) => {
return (
<EditorRow>
<EditorFieldGroup>
<EditorField label="Table">
<EditorField label={t('components.table-section.label-table', 'Table')}>
<Select
aria-label="Table"
aria-label={t('components.table-section.aria-label-table', 'Table')}
value={builderQuery?.from?.property.name}
options={tableOptions}
placeholder="Select a table"
placeholder={t('components.table-section.placeholder-select-table', 'Select a table')}
onChange={handleTableChange}
width={inputFieldSize}
isLoading={isLoadingSchema}
/>
</EditorField>
<EditorField label="Columns">
<EditorField label={t('components.table-section.label-columns', 'Columns')}>
<InputGroup>
<Select
aria-label="Columns"
aria-label={t('components.table-section.aria-label-columns', 'Columns')}
isMulti
isClearable
closeMenuOnSelect={false}
value={columnSelectValue}
options={selectableOptions}
placeholder="Select columns"
placeholder={t('components.table-section.placeholder-select-columns', 'Select columns')}
onChange={handleColumnsChange}
isDisabled={!builderQuery?.from?.property.name}
width={30}

@ -2,8 +2,9 @@ import { css } from '@emotion/css';
import { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { AccessoryButton } from '@grafana/plugin-ui';
import { Icon, Input, Tooltip, Label, Button, useStyles2 } from '@grafana/ui';
import { Icon, Input, Tooltip, Label, Button, useStyles2, TextLink } from '@grafana/ui';
export interface ResourcePickerProps<T> {
resources: T[];
@ -16,6 +17,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<string>) => {
const styles = useStyles2(getStyles);
const { t } = useTranslate();
useEffect(() => {
// Ensure there is at least one resource
@ -44,20 +46,19 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<str
<>
<Label>
<h6>
Resource URI(s){' '}
<Trans i18nKey="components.advanced-resource-picker.label-resource-uri">Resource URI(s) </Trans>
<Tooltip
content={
<>
<Trans i18nKey="components.advanced-resource-picker.tooltip-resource-uri">
Manually edit the{' '}
<a
<TextLink
href="https://docs.microsoft.com/en-us/azure/azure-monitor/logs/log-standard-columns#_resourceid"
rel="noopener noreferrer"
target="_blank"
external
>
resource uri
</a>
</TextLink>
. Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)
</>
</Trans>
}
placement="right"
interactive={true}
@ -73,11 +74,12 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<str
id={`input-advanced-resource-picker-${index + 1}`}
value={resource}
onChange={(event) => onResourceChange(index, event.currentTarget.value)}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="ex: /subscriptions/$subId"
data-testid={`input-advanced-resource-picker-${index + 1}`}
/>
<AccessoryButton
aria-label="remove"
aria-label={t('components.advanced-resource-picker.aria-label-remove', 'Remove')}
icon="times"
variant="secondary"
onClick={() => removeResource(index)}
@ -87,8 +89,14 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<str
</div>
</div>
))}
<Button aria-label="Add" icon="plus" variant="secondary" onClick={addResource} type="button">
Add resource URI
<Button
aria-label={t('components.advanced-resource-picker.aria-label-add', 'Add')}
icon="plus"
variant="secondary"
onClick={addResource}
type="button"
>
<Trans i18nKey="components.advanced-resource-picker.button-add-resource-uri">Add resource URI</Trans>
</Button>
</>
);

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import {
Button,
@ -41,6 +42,7 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
const [isLoading, setIsLoading] = useState(true);
const [searchInputValue, setSearchInputValue] = useState('');
const styles = useStyles2(getStyles);
const { t } = useTranslate();
const lang = { grammar: tokenizer, name: 'kql' };
const dropdownMenu = useMemo(() => {
if (cheatsheetQueries) {
@ -144,17 +146,20 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
const filteredQueries = filterQueriesBySearch(e.currentTarget.value);
setVisibleQueries(filteredQueries);
}}
placeholder="Search Logs queries"
placeholder={t('components.azure-cheat-sheet.placeholder-search-logs', 'Search Logs queries')}
width={40}
/>
<Field label="Categories" className={styles.categoryDropdown}>
<Field
label={t('components.azure-cheat-sheet.label-categories', 'Categories')}
className={styles.categoryDropdown}
>
<Select
options={dropdownMenu}
value={''}
onChange={(a) => filterQueriesByCategory(a)}
allowCustomValue={false}
backspaceRemovesValue={true}
placeholder="All categories"
placeholder={t('components.azure-cheat-sheet.placeholder-all-categories', 'All categories')}
isClearable={true}
noOptionsMessage="Unable to list all categories"
formatCreateLabel={(input: string) => `Category: ${input}`}
@ -165,11 +170,17 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
</Field>
</div>
<div className={styles.spacing}>
Query results:{' '}
{Object.keys(visibleQueries).reduce((totalQueries: number, category) => {
totalQueries = visibleQueries[category]!.length + totalQueries;
return totalQueries;
}, 0)}
<Trans
i18nKey="components.azure-cheat-sheet.label-query-results"
values={{
numResults: Object.keys(visibleQueries).reduce((totalQueries: number, category) => {
totalQueries = visibleQueries[category]!.length + totalQueries;
return totalQueries;
}, 0),
}}
>
Query results: {'{{numResults}}'}
</Trans>
</div>
<ScrollContainer showScrollIndicators maxHeight="350px">
{Object.keys(visibleQueries).map((category: string) => {
@ -188,7 +199,11 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
<Card.Heading>{query.displayName}</Card.Heading>
<ScrollContainer showScrollIndicators maxHeight="100px">
<RawQuery
aria-label={`${query.displayName} raw query`}
aria-label={t(
'components.azure-cheat-sheet.aria-label-raw-query',
'{{queryDisplayName}} raw query',
{ queryDisplayName: query.displayName }
)}
query={query.body}
lang={lang}
className={styles.rawQuery}
@ -197,7 +212,10 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
<Card.Actions>
<Button
size="sm"
aria-label="use this query button"
aria-label={t(
'components.azure-cheat-sheet.aria-label-use-query',
'Use this query button'
)}
onClick={() => {
props.onChange({
refId: 'A',
@ -213,7 +231,7 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
});
}}
>
Use this query
<Trans i18nKey="components.azure-cheat-sheet.button-use-query">Use this query</Trans>
</Button>
</Card.Actions>
</Card>
@ -227,7 +245,7 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => {
</ScrollContainer>
</div>
) : (
<LoadingPlaceholder text="Loading..." />
<LoadingPlaceholder text={t('components.azure-cheat-sheet.text-loading', 'Loading...')} />
)}
</div>
);

@ -1,4 +1,5 @@
import { CoreApp } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Modal } from '@grafana/ui';
import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource';
@ -17,9 +18,15 @@ type Props = {
export const AzureCheatSheetModal = (props: Props) => {
const { isOpen, onClose, datasource, onChange } = props;
const { t } = useTranslate();
return (
<Modal aria-label="Kick start your query modal" isOpen={isOpen} title="Kick start your query" onDismiss={onClose}>
<Modal
aria-label={t('components.azure-cheat-sheet-modal.aria-label-kick-start', 'Kick start your query modal')}
isOpen={isOpen}
title={t('components.azure-cheat-sheet-modal.title-kick-start', 'Kick start your query')}
onDismiss={onClose}
>
<AzureCheatSheet
onChange={(a) => {
onChange(a);

@ -1,5 +1,6 @@
import { useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { ConfirmModal, InlineField, RadioButtonGroup } from '@grafana/ui';
import { AzureQueryEditorFieldProps } from '../../types';
@ -8,13 +9,17 @@ import { setBasicLogsQuery, setDashboardTime, setKustoQuery } from './setQueryVa
export function LogsManagement({ query, onQueryChange: onChange }: AzureQueryEditorFieldProps) {
const [basicLogsAckOpen, setBasicLogsAckOpen] = useState<boolean>(false);
const { t } = useTranslate();
return (
<>
<ConfirmModal
isOpen={basicLogsAckOpen}
title="Basic Logs Queries"
title={t('components.logs-management.title-basic-logs-queries', 'Basic Logs Queries')}
body="Are you sure you want to switch to Basic Logs?"
description="Basic Logs queries incur cost based on the amount of data scanned."
description={t(
'components.logs-management.description-basic-logs-queries',
'Basic Logs queries incur cost based on the amount of data scanned.'
)}
confirmText="Confirm"
onConfirm={() => {
setBasicLogsAckOpen(false);
@ -29,7 +34,13 @@ export function LogsManagement({ query, onQueryChange: onChange }: AzureQueryEdi
}}
confirmButtonVariant="primary"
/>
<InlineField label="Logs" tooltip={<span>Specifies whether to run a Basic or Analytics Logs query.</span>}>
<InlineField
label={t('components.logs-management.label-logs', 'Logs')}
tooltip={t(
'components.logs-management.tooltip-logs',
'Specifies whether to run a Basic or Analytics Logs query.'
)}
>
<RadioButtonGroup
options={[
{ label: 'Analytics', value: false },

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { PanelData, TimeRange } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui';
import { config, getTemplateSrv } from '@grafana/runtime';
import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui';
@ -190,7 +191,7 @@ const LogsQueryEditor = ({
href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1"
external
>
Learn More
<Trans i18nKey="components.logs-query-editor.learn-more">Learn More</Trans>
</TextLink>
</Text>
</>
@ -218,7 +219,7 @@ const LogsQueryEditor = ({
style={{ marginTop: '22px' }}
href={querySeries.meta?.custom?.azurePortalLink}
>
View query in Azure Portal
<Trans i18nKey="components.logs-query-editor.view-query">View query in Azure Portal</Trans>
</LinkButton>
</>
);

@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
import Prism, { Grammar } from 'prismjs';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { useTheme2 } from '@grafana/ui';
export interface Props {
@ -14,13 +15,14 @@ export interface Props {
}
export function RawQuery({ query, lang, className }: Props) {
const theme = useTheme2();
const { t } = useTranslate();
const styles = getStyles(theme);
const highlighted = Prism.highlight(query, lang.grammar, lang.name);
return (
<div
className={cx(styles.editorField, 'prism-syntax-highlight', className)}
aria-label="selector"
aria-label={t('components.raw-query.aria-label-selector', 'Selector')}
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
);

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { InlineField, RadioButtonGroup, Select } from '@grafana/ui';
import { AzureQueryEditorFieldProps } from '../../types';
@ -11,6 +12,7 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure
const [defaultTimeColumns, setDefaultTimeColumns] = useState<SelectableValue[] | undefined>();
const [timeColumns, setTimeColumns] = useState<SelectableValue[] | undefined>();
const [disabledTimePicker, setDisabledTimePicker] = useState<boolean>(false);
const { t } = useTranslate();
const setDefaultColumn = useCallback((column: string) => onChange(setTimeColumn(query, column)), [query, onChange]);
@ -89,13 +91,15 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure
return (
<>
<InlineField
label="Time-range"
label={t('components.time-management.label-time-range', 'Time-range')}
tooltip={
<span>
Specifies the time-range used to query. The <code>Query</code> option will only use time-ranges specified in
the query. <code>Dashboard</code> will only use the Grafana time-range. In Logs Builder mode, only Dashboard
time will be used.
</span>
<Trans i18nKey="components.time-management.tooltip-time-range">
<span>
Specifies the time-range used to query. The <code>Query</code> option will only use time-ranges specified
in the query. <code>Dashboard</code> will only use the Grafana time-range. In Logs Builder mode, only
Dashboard time will be used.
</span>
</Trans>
}
>
<RadioButtonGroup
@ -116,12 +120,14 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure
</InlineField>
{(query.azureLogAnalytics?.dashboardTime || query.azureLogAnalytics?.mode === 'builder') && (
<InlineField
label="Time Column"
label={t('components.time-management.label-time-column', 'Time Column')}
tooltip={
<span>
Specifies the time column used for filtering. Defaults to the first tables <code>timeSpan</code> column,
the first <code>datetime</code> column found or <code>TimeGenerated</code>.
</span>
<Trans i18nKey="components.time-management.tooltip-time-column">
<span>
Specifies the time column used for filtering. Defaults to the first tables <code>timeSpan</code> column,
the first <code>datetime</code> column found or <code>TimeGenerated</code>.
</span>
</Trans>
}
>
<Select

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { AccessoryButton } from '@grafana/plugin-ui';
import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui';
@ -21,6 +22,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
});
const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<AzureMonitorResource>) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
useEffect(() => {
@ -60,7 +62,7 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
return (
<>
<InlineField
label="Subscription"
label={t('components.advanced-resource-picker.label-subscription', 'Subscription')}
grow
transparent
htmlFor={`input-advanced-resource-picker-subscription`}
@ -71,11 +73,12 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
id={`input-advanced-resource-picker-subscription`}
value={resources[0]?.subscription ?? ''}
onChange={(event) => onCommonPropChange({ subscription: event.currentTarget.value })}
placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee"
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX"
/>
</InlineField>
<InlineField
label="Namespace"
label={t('components.advanced-resource-picker.label-namespace', 'Namespace')}
grow
transparent
htmlFor={`input-advanced-resource-picker-metricNamespace`}
@ -88,31 +91,45 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
id={`input-advanced-resource-picker-metricNamespace`}
value={resources[0]?.metricNamespace ?? ''}
onChange={(event) => onCommonPropChange({ metricNamespace: event.currentTarget.value })}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="Microsoft.Insights/metricNamespaces"
/>
</InlineField>
<InlineField
label="Region"
label={t('components.advanced-resource-picker.label-region', 'Region')}
grow
transparent
htmlFor={`input-advanced-resource-picker-region`}
labelWidth={15}
data-testid={selectors.components.queryEditor.resourcePicker.advanced.region.input}
tooltip="The code region of the resource. Optional for one resource but mandatory when selecting multiple ones."
tooltip={t(
'components.advanced-resource-picker.tooltip-region',
'The code region of the resource. Optional for one resource but mandatory when selecting multiple ones.'
)}
>
<Input
id={`input-advanced-resource-picker-region`}
value={resources[0]?.region ?? ''}
onChange={(event) => onCommonPropChange({ region: event.currentTarget.value })}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="northeurope"
/>
</InlineField>
<div className={styles.resourceList}>
{resources.map((resource, index) => (
<div key={`resource-${index + 1}`} className={styles.resource}>
{resources.length !== 1 && <Label className={styles.resourceLabel}>Resource {index + 1}</Label>}
{resources.length !== 1 && (
<Label className={styles.resourceLabel}>
<Trans
i18nKey="components.advanced-resource-picker.label-resource-number"
values={{ resourceNum: index + 1 }}
>
Resource {'{{resourceNum}}'}
</Trans>
</Label>
)}
<InlineField
label="Resource Group"
label={t('components.advanced-resource-picker.label-resource-group', 'Resource Group')}
transparent
htmlFor={`input-advanced-resource-picker-resourceGroup-${index + 1}`}
labelWidth={15}
@ -125,10 +142,11 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
onChange={(event) =>
onResourceChange(index, { ...resource, resourceGroup: event.currentTarget.value })
}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="resource-group"
/>
<AccessoryButton
aria-label="remove"
aria-label={t('components.advanced-resource-picker.aria-label-remove', 'Remove')}
icon="times"
variant="secondary"
onClick={() => removeResource(index)}
@ -139,7 +157,7 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
</InlineField>
<InlineField
label="Resource Name"
label={t('components.advanced-resource-picker.label-resource-name', 'Resource Name')}
transparent
htmlFor={`input-advanced-resource-picker-resourceName-${index + 1}`}
labelWidth={15}
@ -149,14 +167,20 @@ const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps<Azu
id={`input-advanced-resource-picker-resourceName-${index + 1}`}
value={resource?.resourceName ?? ''}
onChange={(event) => onResourceChange(index, { ...resource, resourceName: event.currentTarget.value })}
placeholder="name"
placeholder={t('components.advanced-resource-picker.placeholder-resource-name', 'name')}
/>
</InlineField>
</div>
))}
</div>
<Button aria-label="Add" icon="plus" variant="secondary" onClick={addResource} type="button">
Add resource
<Button
aria-label={t('components.advanced-resource-picker.aria-label-add', 'Add')}
icon="plus"
variant="secondary"
onClick={addResource}
type="button"
>
<Trans i18nKey="components.advanced-resource-picker.button-add-resource">Add resource</Trans>
</Button>
</>
);

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
@ -21,6 +22,7 @@ const AggregationField = ({
aggregationOptions,
isLoading,
}: AggregationFieldProps) => {
const { t } = useTranslate();
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
@ -36,7 +38,7 @@ const AggregationField = ({
const options = addValueToOptions(aggregationOptions, variableOptionGroup, query.azureMonitor?.aggregation);
return (
<Field label="Aggregation">
<Field label={t('components.aggregation-field.label-aggregation', 'Aggregation')}>
<Select
inputId="azure-monitor-metrics-aggregation-field"
value={query.azureMonitor?.aggregation || null}

@ -237,7 +237,7 @@ describe(`Azure Monitor QueryEditor`, () => {
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
const labelSelect = screen.getByTestId('dimension-labels-select');
await openMenu(labelSelect);
const options = await screen.findAllByTestId(selectors.components.Select.option);
expect(options).toHaveLength(2);
@ -285,7 +285,7 @@ describe(`Azure Monitor QueryEditor`, () => {
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
const labelSelect = screen.getByTestId('dimension-labels-select');
await user.click(labelSelect);
await openMenu(labelSelect);
screen.getByText('testlabel');
@ -323,7 +323,7 @@ describe(`Azure Monitor QueryEditor`, () => {
dimensionOptions={dimensionOptions}
/>
);
const labelSelect2 = screen.getByLabelText('dimension-labels-select');
const labelSelect2 = screen.getByTestId('dimension-labels-select');
await openMenu(labelSelect2);
const refreshedOptions = await screen.findAllByLabelText('Select options menu');
expect(refreshedOptions).toHaveLength(1);

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorList, AccessoryButton } from '@grafana/plugin-ui';
import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui';
@ -62,6 +63,7 @@ const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuer
};
const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: DimensionFieldsProps) => {
const { t } = useTranslate();
const dimensionFilters = useMemo(
() => query.azureMonitor?.dimensionFilters ?? [],
[query.azureMonitor?.dimensionFilters]
@ -150,14 +152,14 @@ const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: Dimen
<HorizontalGroup spacing="none">
<Select
menuShouldPortal
placeholder="Field"
placeholder={t('components.dimension-fields.placeholder-field', 'Field')}
value={item.dimension}
options={getValidDimensionOptions(item.dimension || '')}
onChange={(e) => onFieldChange('dimension', item, e.value ?? '', onChange)}
/>
<Select
menuShouldPortal
placeholder="Operation"
placeholder={t('components.dimension-fields.placeholder-operation', 'Operation')}
value={item.operator}
options={getValidOperators(item.operator || 'eq')}
onChange={(e) => onFieldChange('operator', item, e.value ?? '', onChange)}
@ -166,7 +168,7 @@ const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: Dimen
{item.operator === 'eq' || item.operator === 'ne' ? (
<MultiSelect
menuShouldPortal
placeholder="Select value(s)"
placeholder={t('components.dimension-fields.placeholder-select-values', 'Select value(s)')}
value={item.filters}
options={getValidMultiSelectOptions(item.filters, item.dimension ?? '')}
onChange={(e) =>
@ -177,14 +179,14 @@ const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: Dimen
onChange
)
}
aria-label={'dimension-labels-select'}
data-testid="dimension-labels-select"
allowCustomValue
/>
) : (
// The API does not currently allow for multiple "starts with" clauses to be used.
<Select
menuShouldPortal
placeholder="Select value"
placeholder={t('components.dimension-fields.placeholder-select-value', 'Select value')}
value={item.filters ? item.filters[0] : ''}
allowCustomValue
options={getValidFilterOptions(item.filters ? item.filters[0] : '', item.dimension ?? '')}
@ -192,13 +194,19 @@ const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: Dimen
isClearable
/>
)}
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
<AccessoryButton
aria-label={t('components.dimension-fields.aria-label-remove', 'Remove')}
icon="times"
variant="secondary"
onClick={onDelete}
type="button"
/>
</HorizontalGroup>
);
};
return (
<Field label="Dimensions">
<Field label={t('components.dimension-fields.label-dimensions', 'Dimensions')}>
<EditorList items={dimensionFilters} onChange={changedFunc} renderItem={renderFilters} />
</Field>
);

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
import { useTranslate } from '@grafana/i18n';
import { Input } from '@grafana/ui';
import { AzureQueryEditorFieldProps } from '../../types';
@ -10,6 +11,7 @@ import { setLegendAlias } from './setQueryValue';
const LegendFormatField = ({ onQueryChange, query }: AzureQueryEditorFieldProps) => {
const [value, setValue] = useState<string>(query.azureMonitor?.alias ?? '');
const { t } = useTranslate();
// As calling onQueryChange initiates a the datasource refresh, we only want to call it once
// the field loses focus
@ -25,10 +27,10 @@ const LegendFormatField = ({ onQueryChange, query }: AzureQueryEditorFieldProps)
}, [onQueryChange, query, value]);
return (
<Field label="Legend format">
<Field label={t('components.legend-format-field.label-legend-format', 'Legend format')}>
<Input
id="azure-monitor-metrics-legend-field"
placeholder="Alias patterns"
placeholder={t('components.legend-format-field.placeholder-legend-format', 'Alias patterns')}
value={value}
onChange={handleChange}
onBlur={handleBlur}

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -15,6 +16,7 @@ interface MetricNameProps extends AzureQueryEditorFieldProps {
}
const MetricNameField = ({ metricNames, query, variableOptionGroup, onQueryChange }: MetricNameProps) => {
const { t } = useTranslate();
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
@ -30,7 +32,10 @@ const MetricNameField = ({ metricNames, query, variableOptionGroup, onQueryChang
const options = addValueToOptions(metricNames, variableOptionGroup, query.azureMonitor?.metricName);
return (
<Field label="Metric" data-testid={selectors.components.queryEditor.metricsQueryEditor.metricName.input}>
<Field
label={t('components.metric-name-field.label-metric', 'Metric')}
data-testid={selectors.components.queryEditor.metricsQueryEditor.metricName.input}
>
<Select
inputId="azure-monitor-metrics-metric-field"
value={query.azureMonitor?.metricName ?? null}

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
@ -19,6 +20,7 @@ const MetricNamespaceField = ({
variableOptionGroup,
onQueryChange,
}: MetricNamespaceFieldProps) => {
const { t } = useTranslate();
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
@ -35,7 +37,7 @@ const MetricNamespaceField = ({
const options = addValueToOptions(metricNamespaces, variableOptionGroup, value);
return (
<Field label="Metric namespace">
<Field label={t('components.metric-namespace-field.label-metric-namespace', 'Metric namespace')}>
<Select
inputId="azure-monitor-metrics-metric-namespace-field"
value={value || null}

@ -1,6 +1,7 @@
import { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import TimegrainConverter from '../../time_grain_converter';
@ -15,6 +16,7 @@ interface TimeGrainFieldProps extends AzureQueryEditorFieldProps {
}
const TimeGrainField = ({ query, timeGrainOptions, variableOptionGroup, onQueryChange }: TimeGrainFieldProps) => {
const { t } = useTranslate();
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
@ -50,7 +52,7 @@ const TimeGrainField = ({ query, timeGrainOptions, variableOptionGroup, onQueryC
}, [timeGrainOptions, variableOptionGroup, query.azureMonitor?.timeGrain]);
return (
<Field label="Time grain">
<Field label={t('components.time-grain-field.label-time-grain', 'Time grain')}>
<Select
inputId="azure-monitor-metrics-time-grain-field"
value={query.azureMonitor?.timeGrain}

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
import { useTranslate } from '@grafana/i18n';
import { Input } from '@grafana/ui';
import { AzureQueryEditorFieldProps } from '../../types';
@ -9,6 +10,7 @@ import { Field } from '../shared/Field';
import { setTop } from './setQueryValue';
const TopField = ({ onQueryChange, query }: AzureQueryEditorFieldProps) => {
const { t } = useTranslate();
const [value, setValue] = useState<string>(query.azureMonitor?.top ?? '');
// As calling onQueryChange initiates a the datasource refresh, we only want to call it once
@ -25,7 +27,7 @@ const TopField = ({ onQueryChange, query }: AzureQueryEditorFieldProps) => {
}, [onQueryChange, query, value]);
return (
<Field label="Top">
<Field label={t('components.top-field.label-top', 'Top')}>
<Input
id="azure-monitor-metrics-top-field"
value={value}

@ -3,8 +3,9 @@ import { useCallback, useMemo, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { CoreApp, QueryEditorProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Alert, CodeEditor, Space } from '@grafana/ui';
import { Alert, CodeEditor, Space, TextLink } from '@grafana/ui';
import AzureMonitorDatasource from '../../datasource';
import { selectors } from '../../e2e/selectors';
@ -44,6 +45,7 @@ const QueryEditor = ({
const onRunQuery = useMemo(() => debounce(baseOnRunQuery, 500), [baseOnRunQuery]);
const [azureLogsCheatSheetModalOpen, setAzureLogsCheatSheetModalOpen] = useState(false);
const [defaultSubscriptionId, setDefaultSubscriptionId] = useState('');
const { t } = useTranslate();
const onQueryChange = useCallback(
(newQuery: AzureMonitorQuery) => {
@ -119,7 +121,13 @@ const QueryEditor = ({
{errorMessage && (
<>
<Space v={2} />
<Alert severity="error" title="An error occurred while requesting metadata from Azure Monitor">
<Alert
severity="error"
title={t(
'components.query-editor.alert-error-occurred',
'An error occurred while requesting metadata from Azure Monitor'
)}
>
{errorMessage instanceof Error ? errorMessage.message : errorMessage}
</Alert>
</>
@ -149,6 +157,7 @@ const EditorForQueryType = ({
onQueryChange,
range,
}: EditorForQueryTypeProps) => {
const { t } = useTranslate();
switch (query.queryType) {
case AzureQueryType.AzureMonitor:
return (
@ -207,18 +216,19 @@ const EditorForQueryType = ({
default:
const type = query.queryType as unknown;
return (
<Alert title="Unknown query type">
<Alert title={t('components.editor-for-query-type.title-unknown-query-type', 'Unknown query type')}>
{(type === 'Application Insights' || type === 'Insights Analytics') && (
<>
{type} was deprecated in Grafana 9. See the{' '}
<a
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#application-insights-and-insights-analytics-removed"
target="_blank"
rel="noreferrer"
>
deprecation notice
</a>{' '}
to get more information about how to migrate your queries. This is the current query definition:
<Trans i18nKey="components.editor-for-query-type.body-unknown-query-type">
{{ type }} was deprecated in Grafana 9. See the{' '}
<TextLink
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#application-insights-and-insights-analytics-removed"
external
>
deprecation notice
</TextLink>{' '}
to get more information about how to migrate your queries. This is the current query definition:
</Trans>
<CodeEditor height="200px" readOnly language="json" value={JSON.stringify(query, null, 4)} />
</>
)}
@ -228,42 +238,48 @@ const EditorForQueryType = ({
};
const UserAuthAlert = () => {
const { t } = useTranslate();
return (
<Alert title="Unsupported authentication provider" data-testid={selectors.components.queryEditor.userAuthAlert}>
<>
<Alert
title={t('components.user-auth-alert.title-unsupported-auth', 'Unsupported authentication provider')}
data-testid={selectors.components.queryEditor.userAuthAlert}
>
<Trans i18nKey="components.user-auth-alert.body-unsupported-auth">
Usage of this data source requires you to be authenticated via Azure Entra (formerly Azure Active Directory).
Please review the{' '}
<a
<TextLink
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#configure-current-user-authentication"
target="_blank"
rel="noreferrer"
external
>
documentation
</a>{' '}
</TextLink>{' '}
for more information.
</>
</Trans>
</Alert>
);
};
const UserAuthFallbackAlert = () => {
const { t } = useTranslate();
return (
<Alert
title="No fallback credentials available"
title={t(
'components.user-auth-fallback-alert.title-no-fallback-credentials',
'No fallback credentials available'
)}
data-testid={selectors.components.queryEditor.userAuthFallbackAlert}
>
<>
<Trans i18nKey="components.user-auth-fallback-alert.body-no-fallback-credentials">
Data source backend features (such as alerting) require service credentials to function. This data source is
configured without service credential fallback, or the fallback functionality is disabled. Please review the{' '}
<a
<TextLink
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#configure-current-user-authentication"
target="_blank"
rel="noreferrer"
external
>
documentation
</a>{' '}
</TextLink>{' '}
for more information.
</>
</Trans>
</Alert>
);
};

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { CoreApp, LoadingState, PanelData, SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { EditorHeader, FlexItem, InlineSelect } from '@grafana/plugin-ui';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui';
@ -35,6 +36,7 @@ export const QueryHeader = ({
const [showModeSwitchWarning, setShowModeSwitchWarning] = useState(false);
const [pendingModeChange, setPendingModeChange] = useState<LogsEditorMode | null>(null);
const { t } = useTranslate();
const currentMode = query.azureLogAnalytics?.mode;
@ -109,7 +111,7 @@ export const QueryHeader = ({
<EditorHeader>
<ConfirmModal
isOpen={showModeSwitchWarning}
title="Switch editor mode?"
title={t('components.query-header.title-switch-mode', 'Switch editor mode?')}
body={
pendingModeChange === LogsEditorMode.Builder
? 'Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?'
@ -130,16 +132,16 @@ export const QueryHeader = ({
/>
<InlineSelect
label="Service"
label={t('components.query-header.label-service', 'Service')}
value={query.queryType === AzureQueryType.TraceExemplar ? AzureQueryType.AzureTraces : query.queryType}
placeholder="Service..."
placeholder={t('components.query-header.placeholder-service', 'Service...')}
allowCustomValue
options={queryTypes}
onChange={handleChange}
/>
{query.queryType === AzureQueryType.LogAnalytics && query.azureLogAnalytics?.mode === LogsEditorMode.Raw && (
<Button
aria-label="Azure logs kick start your query button"
aria-label={t('components.query-header.aria-label-kick-start', 'Azure logs kick start your query button')}
variant="secondary"
size="sm"
onClick={() => {
@ -150,7 +152,7 @@ export const QueryHeader = ({
});
}}
>
Kick start your query
<Trans i18nKey="components.query-header.button-kick-start-your-query">Kick start your query</Trans>
</Button>
)}
<FlexItem grow={1} />
@ -173,7 +175,7 @@ export const QueryHeader = ({
onClick={onRunQuery}
data-testid={selectors.components.queryEditor.logsQueryEditor.runQuery.button}
>
Run query
<Trans i18nKey="components.query-header.button-run-query">Run query</Trans>
</Button>
)}
</EditorHeader>

@ -2,6 +2,7 @@ import { cx } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import * as React from 'react';
import { Trans, useTranslate } from '@grafana/i18n';
import { Button, Icon, Modal, useStyles2, IconName } from '@grafana/ui';
import Datasource from '../../datasource';
@ -42,6 +43,7 @@ const ResourceField = ({
}: Props) => {
const styles = useStyles2(getStyles);
const [pickerIsOpen, setPickerIsOpen] = useState(false);
const { t } = useTranslate();
const handleOpenPicker = useCallback(() => {
setPickerIsOpen(true);
@ -63,7 +65,7 @@ const ResourceField = ({
<span data-testid={selectors.components.queryEditor.resourcePicker.select.button}>
<Modal
className={styles.modal}
title="Select a resource"
title={t('components.resource-field.title-select-resource', 'Select a resource')}
isOpen={pickerIsOpen}
onDismiss={closePicker}
// The growing number of rows added to the modal causes a focus
@ -82,7 +84,11 @@ const ResourceField = ({
selectionNotice={selectionNotice}
/>
</Modal>
<Field label="Resource" inlineField={inlineField} labelWidth={labelWidth}>
<Field
label={t('components.resource-field.label-resource', 'Resource')}
inlineField={inlineField}
labelWidth={labelWidth}
>
<Button className={styles.resourceFieldButton} variant="secondary" onClick={handleOpenPicker} type="button">
<ResourceLabel resources={resources} datasource={datasource} />
</Button>
@ -104,7 +110,7 @@ const ResourceLabel = ({ resources, datasource }: ResourceLabelProps<string | Az
}, [resources]);
if (!resources.length) {
return <>Select a resource</>;
return <Trans i18nKey="components.resource-label.select-resource">Select a resource</Trans>;
}
return <FormattedResource resources={resourcesComponents} />;

@ -1,6 +1,7 @@
import { useState } from 'react';
import * as React from 'react';
import { useTranslate } from '@grafana/i18n';
import { Collapse, Space } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -14,12 +15,13 @@ export interface ResourcePickerProps<T> {
const AdvancedMulti = ({ resources, onChange, renderAdvanced }: ResourcePickerProps<string | AzureMonitorResource>) => {
const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$'));
const { t } = useTranslate();
return (
<div data-testid={selectors.components.queryEditor.resourcePicker.advanced.collapse}>
<Collapse
collapsible
label="Advanced"
label={t('components.advanced-multi.label-advanced', 'Advanced')}
isOpen={isAdvancedOpen}
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
>

@ -1,6 +1,7 @@
import { cx } from '@emotion/css';
import { useEffect, useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { FadeTransition, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { NestedEntry } from './NestedEntry';
@ -31,6 +32,7 @@ const NestedRow = ({
}: NestedRowProps) => {
const styles = useStyles2(getStyles);
const [rowStatus, setRowStatus] = useState<'open' | 'closed' | 'loading'>('closed');
const { t } = useTranslate();
const isSelected = !!selectedRows.find((v) => v.uri.toLowerCase() === row.uri.toLowerCase());
const isDisabled = !isSelected && disableRow(row, selectedRows);
@ -101,7 +103,10 @@ const NestedRow = ({
<FadeTransition visible={rowStatus === 'loading'}>
<tr>
<td className={cx(styles.cell, styles.loadingCell)} colSpan={3}>
<LoadingPlaceholder text="Loading..." className={styles.spinner} />
<LoadingPlaceholder
text={t('components.nested-row.text-loading', 'Loading...')}
className={styles.spinner}
/>
</td>
</tr>
</FadeTransition>

@ -260,7 +260,7 @@ describe('AzureMonitor ResourcePicker', () => {
const searchRow1 = screen.queryByLabelText('search-result');
expect(searchRow1).not.toBeInTheDocument();
const searchField = await screen.findByLabelText('resource search');
const searchField = await screen.findByLabelText('Resource search');
expect(searchField).toBeInTheDocument();
await userEvent.type(searchField, 'sea');
@ -275,7 +275,7 @@ describe('AzureMonitor ResourcePicker', () => {
render(<ResourcePicker {...defaultProps} resourcePickerData={rpd} />);
const searchField = await screen.findByLabelText('resource search');
const searchField = await screen.findByLabelText('Resource search');
expect(searchField).toBeInTheDocument();
await userEvent.type(searchField, 'some search that has no results');
@ -294,7 +294,7 @@ describe('AzureMonitor ResourcePicker', () => {
render(<ResourcePicker {...defaultProps} resourcePickerData={rpd} />);
const searchField = await screen.findByLabelText('resource search');
const searchField = await screen.findByLabelText('Resource search');
expect(searchField).toBeInTheDocument();
await userEvent.type(searchField, 'sear');
@ -320,7 +320,7 @@ describe('AzureMonitor ResourcePicker', () => {
const searchRow1 = screen.queryByLabelText('search-result');
expect(searchRow1).not.toBeInTheDocument();
const searchField = await screen.findByLabelText('resource search');
const searchField = await screen.findByLabelText('Resource search');
expect(searchField).toBeInTheDocument();
await userEvent.type(searchField, 'sea');

@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
import * as React from 'react';
import { useEffectOnce } from 'react-use';
import { Trans, useTranslate } from '@grafana/i18n';
import { Alert, Button, LoadingPlaceholder, Modal, useStyles2, Space } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -42,6 +43,7 @@ const ResourcePicker = ({
selectionNotice,
}: ResourcePickerProps<string | AzureMonitorResource>) => {
const styles = useStyles2(getStyles);
const { t } = useTranslate();
const [isLoading, setIsLoading] = useState(false);
const [rows, setRows] = useState<ResourceRowGroup>([]);
@ -173,7 +175,14 @@ const ResourcePicker = ({
<>
<Search searchFn={handleSearch} />
{shouldShowLimitFlag ? (
<p className={styles.resultLimit}>Showing first {resourcePickerData.resultLimit} results</p>
<p className={styles.resultLimit}>
<Trans
i18nKey="components.resource-picker.result-limit"
values={{ numResults: resourcePickerData.resultLimit }}
>
Showing first {'{{numResults}}'} results
</Trans>
</p>
) : (
<Space v={2} />
)}
@ -181,9 +190,15 @@ const ResourcePicker = ({
<table className={styles.table}>
<thead>
<tr className={cx(styles.row, styles.header)}>
<td className={styles.cell}>Scope</td>
<td className={styles.cell}>Type</td>
<td className={styles.cell}>Location</td>
<td className={styles.cell}>
<Trans i18nKey="components.resource-picker.header-scope">Scope</Trans>
</td>
<td className={styles.cell}>
<Trans i18nKey="components.resource-picker.header-type">Type</Trans>
</td>
<td className={styles.cell}>
<Trans i18nKey="components.resource-picker.header-location">Location</Trans>
</td>
</tr>
</thead>
</table>
@ -194,14 +209,14 @@ const ResourcePicker = ({
{isLoading && (
<tr className={cx(styles.row)}>
<td className={styles.cell}>
<LoadingPlaceholder text={'Loading...'} />
<LoadingPlaceholder text={t('components.resource-picker.text-loading', 'Loading...')} />
</td>
</tr>
)}
{!isLoading && rows.length === 0 && (
<tr className={cx(styles.row)}>
<td className={styles.cell} aria-live="polite">
No resources found
<Trans i18nKey="components.resource-picker.text-no-resources">No resources found</Trans>
</td>
</tr>
)}
@ -226,7 +241,9 @@ const ResourcePicker = ({
<footer className={styles.selectionFooter}>
{selectedRows.length > 0 && (
<>
<h5>Selection</h5>
<h5>
<Trans i18nKey="components.resource-picker.heading-selection">Selection</Trans>
</h5>
<div className={cx(styles.scrollableTable, styles.selectedTableScroller)}>
<table className={styles.table}>
@ -264,7 +281,13 @@ const ResourcePicker = ({
{errorMessage && (
<>
<Space v={2} />
<Alert severity="error" title="An error occurred while requesting resources from Azure Monitor">
<Alert
severity="error"
title={t(
'components.resource-picker.title-error-occurred',
'An error occurred while requesting resources from Azure Monitor'
)}
>
{errorMessage}
</Alert>
</>
@ -272,14 +295,14 @@ const ResourcePicker = ({
<Modal.ButtonRow>
<Button onClick={onCancel} variant="secondary" fill="outline">
Cancel
<Trans i18nKey="components.resource-picker.button-cancel">Cancel</Trans>
</Button>
<Button
disabled={!!errorMessage || !internalSelected.every(isValid)}
onClick={handleApply}
data-testid={selectors.components.queryEditor.resourcePicker.apply.button}
>
Apply
<Trans i18nKey="components.resource-picker.button-apply">Apply</Trans>
</Button>
</Modal.ButtonRow>
</footer>

@ -1,12 +1,14 @@
import { debounce } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { Icon, Input } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
const Search = ({ searchFn }: { searchFn: (searchPhrase: string) => void }) => {
const [searchFilter, setSearchFilter] = useState('');
const { t } = useTranslate();
const debouncedSearch = useMemo(() => debounce(searchFn, 600), [searchFn]);
useEffect(() => {
@ -18,7 +20,7 @@ const Search = ({ searchFn }: { searchFn: (searchPhrase: string) => void }) => {
return (
<Input
aria-label="resource search"
aria-label={t('components.search.aria-label-resource-search', 'Resource search')}
prefix={<Icon name="search" />}
value={searchFilter}
onChange={(event) => {
@ -26,7 +28,7 @@ const Search = ({ searchFn }: { searchFn: (searchPhrase: string) => void }) => {
setSearchFilter(searchPhrase);
debouncedSearch(searchPhrase);
}}
placeholder="search for a resource"
placeholder={t('components.search.placeholder-resource-search', 'Search for a resource')}
data-testid={selectors.components.queryEditor.resourcePicker.search.input}
/>
);

@ -5,6 +5,7 @@ import { lastValueFrom } from 'rxjs';
import { CoreApp, DataFrame, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useTranslate } from '@grafana/i18n';
import { AccessoryButton } from '@grafana/plugin-ui';
import {
HorizontalGroup,
@ -208,6 +209,7 @@ const Filter = (
const [selected, setSelected] = useState<SelectableValue[]>(
item.filters ? item.filters.map((filter) => ({ value: filter, label: filter === '' ? '<Empty>' : filter })) : []
);
const { t } = useTranslate();
const loadOptions = async () => {
setLoading(true);
@ -240,7 +242,7 @@ const Filter = (
<HorizontalGroup spacing="none">
<Select
menuShouldPortal
placeholder="Property"
placeholder={t('components.filter.placeholder-property', 'Property')}
value={item.property ? { value: item.property, label: item.property } : null}
options={addValueToOptions(
properties.map((type) => ({ label: type, value: type })),
@ -261,7 +263,7 @@ const Filter = (
<AsyncMultiSelect
blurInputOnSelect={false}
menuShouldPortal
placeholder="Value"
placeholder={t('components.filter.placeholder-value', 'Value')}
value={selected}
loadOptions={loadOptions}
isLoading={loading}
@ -280,7 +282,13 @@ const Filter = (
onCloseMenu={() => onFieldChange('filters', item, selected, onChange)}
hideSelectedOptions={false}
/>
<AccessoryButton aria-label="Remove filter" icon="times" variant="secondary" onClick={onDelete} type="button" />
<AccessoryButton
aria-label={t('components.filter.aria-label-remove-filter', 'Remove filter')}
icon="times"
variant="secondary"
onClick={onDelete}
type="button"
/>
</HorizontalGroup>
);
};

@ -2,6 +2,7 @@ import { uniq } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorList } from '@grafana/plugin-ui';
import { Field } from '@grafana/ui';
@ -14,6 +15,7 @@ import { setFilters } from './setQueryValue';
const Filters = ({ query, datasource, onQueryChange, variableOptionGroup, range }: AzureQueryEditorFieldProps) => {
const { azureTraces } = query;
const queryTraceTypes = azureTraces?.traceTypes ? azureTraces.traceTypes : Object.keys(tablesSchema);
const { t } = useTranslate();
const excludedProperties = new Set([
'customDimensions',
@ -59,7 +61,7 @@ const Filters = ({ query, datasource, onQueryChange, variableOptionGroup, range
};
return (
<Field label="Filters">
<Field label={t('components.filters.label-filters', 'Filters')}>
<EditorList
items={filters}
onChange={changedFunc}

@ -1,6 +1,7 @@
import { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { MultiSelect } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -12,6 +13,7 @@ import { Tables } from './consts';
import { setTraceTypes } from './setQueryValue';
const TraceTypeField = ({ query, variableOptionGroup, onQueryChange }: AzureQueryEditorFieldProps) => {
const { t } = useTranslate();
const tables: AzureMonitorOption[] = Object.entries(Tables).map(([key, value]) => ({
label: value.label,
description: value.description,
@ -39,9 +41,9 @@ const TraceTypeField = ({ query, variableOptionGroup, onQueryChange }: AzureQuer
};
return (
<Field label="Event Type">
<Field label={t('components.trace-type-field.label-event-type', 'Event Type')}>
<MultiSelect
placeholder="Choose event types"
placeholder={t('components.trace-type-field.placeholder-event-type', 'Choose event types')}
inputId="azure-monitor-traces-type-field"
value={findOptions(
[...tables, ...variableOptionGroup.options],

@ -3,6 +3,7 @@ import * as React from 'react';
import { usePrevious } from 'react-use';
import { TimeRange } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui';
import { Input } from '@grafana/ui';
@ -39,6 +40,7 @@ const TracesQueryEditor = ({
setError,
range,
}: TracesQueryEditorProps) => {
const { t } = useTranslate();
const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => {
if (selectedRows.length === 0) {
// Only if there is some resource(s) selected we should disable rows
@ -127,7 +129,7 @@ const TracesQueryEditor = ({
variableOptionGroup={variableOptionGroup}
range={range}
/>
<Field label="Operation ID">
<Field label={t('components.traces-query-editor.label-operation-id', 'Operation ID')}>
<Input
id="azure-monitor-traces-operation-id-field"
value={operationId}

@ -1,5 +1,6 @@
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { InlineField, Input } from '@grafana/ui';
import DataSource from '../../datasource';
@ -16,6 +17,7 @@ const GrafanaTemplateVariableFnInput = ({
datasource: DataSource;
}) => {
const [inputVal, setInputVal] = useState('');
const { t } = useTranslate();
useEffect(() => {
setInputVal(query.grafanaTemplateVariableFn?.rawQuery || '');
@ -45,9 +47,18 @@ const GrafanaTemplateVariableFnInput = ({
};
return (
<InlineField label="Grafana template variable function">
<InlineField
label={t(
'components.grafana-template-variable-fn-input.label-grafana-template-variable',
'Grafana template variable function'
)}
>
<Input
placeholder={'type a grafana template variable function, ex: Subscriptions()'}
placeholder={t(
'components.grafana-template-variable-fn-input.placeholder-grafana-template-variable',
'Type a grafana template variable function, e.g. {{example}}',
{ example: 'Subscriptions()' }
)}
value={inputVal}
onChange={onChange}
onBlur={() => onRunQuery(inputVal)}

@ -85,10 +85,10 @@ describe('VariableEditor:', () => {
const onChange = jest.fn();
const legacyQuery = { ...defaultProps.query, queryType: AzureQueryType.GrafanaTemplateVariableFn };
render(<VariableEditor {...defaultProps} onChange={onChange} query={legacyQuery} />);
await waitFor(() => screen.getByLabelText('select query type'));
expect(screen.getByLabelText('select query type')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('select query type'));
await select(screen.getByLabelText('select query type'), 'Grafana Query Function', {
await waitFor(() => screen.getByLabelText('Select query type'));
expect(screen.getByLabelText('Select query type')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Select query type'));
await select(screen.getByLabelText('Select query type'), 'Grafana Query Function', {
container: document.body,
});
expect(onChange).toHaveBeenCalledWith(
@ -141,9 +141,9 @@ describe('VariableEditor:', () => {
render(<VariableEditor {...ARGqueryProps} />);
await waitFor(() => screen.queryByTestId('mockeditor'));
await waitFor(() => screen.queryByLabelText('Subscriptions'));
expect(screen.queryByLabelText('Select query type')).toBeInTheDocument();
expect(screen.queryByText('Resource Graph')).toBeInTheDocument();
expect(screen.queryByLabelText('Select subscription')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Select query type')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Select resource group')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Select namespace')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Select resource')).not.toBeInTheDocument();
@ -235,7 +235,7 @@ describe('VariableEditor:', () => {
it('should run the query if requesting subscriptions', async () => {
const onChange = jest.fn();
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
await selectAndRerender('select query type', 'Subscriptions', onChange, rerender);
await selectAndRerender('Select query type', 'Subscriptions', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ queryType: AzureQueryType.SubscriptionsQuery, refId: 'A' })
);
@ -246,8 +246,8 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Resource Groups', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select query type', 'Resource Groups', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.ResourceGroupsQuery,
@ -262,9 +262,9 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Resource Groups', onChange, rerender);
await selectAndRerender('Select query type', 'Resource Groups', onChange, rerender);
// Select a subscription
openMenu(screen.getByLabelText('select subscription'));
openMenu(screen.getByLabelText('Select subscription'));
await waitFor(() => expect(screen.getByText('Primary Subscription')).toBeInTheDocument());
await userEvent.click(screen.getByText('Template Variables'));
// Simulate onChange behavior
@ -280,8 +280,8 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Namespaces', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select query type', 'Namespaces', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.NamespacesQuery,
@ -296,9 +296,9 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Resource Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select region', 'North Europe', onChange, rerender);
await selectAndRerender('Select query type', 'Resource Names', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select region', 'North Europe', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.ResourceNamesQuery,
@ -314,11 +314,11 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Metric Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select resource group', 'rg', onChange, rerender);
await selectAndRerender('select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('select resource', 'foobar', onChange, rerender);
await selectAndRerender('Select query type', 'Metric Names', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select resource group', 'rg', onChange, rerender);
await selectAndRerender('Select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('Select resource', 'foobar', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.MetricNamesQuery,
@ -337,7 +337,7 @@ describe('VariableEditor:', () => {
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
// Select a new query type
await selectAndRerender('select query type', 'Subscriptions', onChange, rerender);
await selectAndRerender('Select query type', 'Subscriptions', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.SubscriptionsQuery,
@ -355,8 +355,8 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Workspaces', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select query type', 'Workspaces', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.WorkspacesQuery,
@ -371,8 +371,8 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Regions', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select query type', 'Regions', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.LocationsQuery,
@ -387,11 +387,11 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Custom Namespaces', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select resource group', 'rg', onChange, rerender);
await selectAndRerender('select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('select resource', 'foobar', onChange, rerender);
await selectAndRerender('Select query type', 'Custom Namespaces', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select resource group', 'rg', onChange, rerender);
await selectAndRerender('Select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('Select resource', 'foobar', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.CustomNamespacesQuery,
@ -409,12 +409,12 @@ describe('VariableEditor:', () => {
const { rerender } = render(<VariableEditor {...defaultProps} onChange={onChange} />);
// wait for initial load
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Custom Metric Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select resource group', 'rg', onChange, rerender);
await selectAndRerender('select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('select resource', 'foobar', onChange, rerender);
await selectAndRerender('select custom namespace', 'foo/custom', onChange, rerender);
await selectAndRerender('Select query type', 'Custom Metric Names', onChange, rerender);
await selectAndRerender('Select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('Select resource group', 'rg', onChange, rerender);
await selectAndRerender('Select namespace', 'foo/bar', onChange, rerender);
await selectAndRerender('Select resource', 'foobar', onChange, rerender);
await selectAndRerender('Select custom namespace', 'foo/custom', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.CustomMetricNamesQuery,

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { getTemplateSrv } from '@grafana/runtime';
import { Alert, Field, Select, Space } from '@grafana/ui';
@ -27,6 +28,7 @@ const removeOption: SelectableValue = { label: '-', value: '' };
const VariableEditor = (props: Props) => {
const { query, onChange, datasource } = props;
const { t } = useTranslate();
const AZURE_QUERY_VARIABLE_TYPE_OPTIONS = [
{ label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery },
{ label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery },
@ -275,9 +277,12 @@ const VariableEditor = (props: Props) => {
return (
<>
<Field label="Query Type" data-testid={selectors.components.variableEditor.queryType.input}>
<Field
label={t('components.variable-editor.label-query-type', 'Query Type')}
data-testid={selectors.components.variableEditor.queryType.input}
>
<Select
aria-label="select query type"
aria-label={t('components.variable-editor.aria-label-select-query-type', 'Select query type')}
onChange={onQueryTypeChange}
options={AZURE_QUERY_VARIABLE_TYPE_OPTIONS}
width={25}
@ -301,7 +306,13 @@ const VariableEditor = (props: Props) => {
{errorMessage && (
<>
<Space v={2} />
<Alert severity="error" title="An error occurred while requesting metadata from Azure Monitor">
<Alert
severity="error"
title={t(
'components.variable-editor.title-error-occurred',
'An error occurred while requesting metadata from Azure Monitor'
)}
>
{errorMessage instanceof Error ? errorMessage.message : errorMessage}
</Alert>
</>
@ -312,9 +323,12 @@ const VariableEditor = (props: Props) => {
<GrafanaTemplateVariableFnInput query={query} updateQuery={props.onChange} datasource={datasource} />
)}
{requireSubscription && (
<Field label="Subscription" data-testid={selectors.components.variableEditor.subscription.input}>
<Field
label={t('components.variable-editor.label-subscription', 'Subscription')}
data-testid={selectors.components.variableEditor.subscription.input}
>
<Select
aria-label="select subscription"
aria-label={t('components.variable-editor.aria-label-select-subscription', 'Select subscription')}
onChange={onChangeSubscription}
options={subscriptions.concat(variableOptionGroup)}
width={25}
@ -323,9 +337,12 @@ const VariableEditor = (props: Props) => {
</Field>
)}
{(requireResourceGroup || hasResourceGroup) && (
<Field label="Resource Group" data-testid={selectors.components.variableEditor.resourceGroup.input}>
<Field
label={t('components.variable-editor.label-resource-group', 'Resource Group')}
data-testid={selectors.components.variableEditor.resourceGroup.input}
>
<Select
aria-label="select resource group"
aria-label={t('components.variable-editor.aria-label-select-resource-group', 'Select resource group')}
onChange={onChangeResourceGroup}
options={
requireResourceGroup
@ -334,7 +351,9 @@ const VariableEditor = (props: Props) => {
}
width={25}
value={query.resourceGroup || null}
placeholder={requireResourceGroup ? undefined : 'Optional'}
placeholder={
requireResourceGroup ? undefined : t('components.variable-editor.placeholder-resource-group', 'Optional')
}
/>
</Field>
)}
@ -342,13 +361,13 @@ const VariableEditor = (props: Props) => {
<Field
label={
queryType === AzureQueryType.CustomNamespacesQuery || queryType === AzureQueryType.CustomMetricNamesQuery
? 'Resource Type'
: 'Namespace'
? t('components.variable-editor.label-resource-type', 'Resource Type')
: t('components.variable-editor.label-namespace', 'Namespace')
}
data-testid={selectors.components.variableEditor.namespace.input}
>
<Select
aria-label="select namespace"
aria-label={t('components.variable-editor.aria-label-select-namespace', 'Select namespace')}
onChange={onChangeNamespace}
options={
requireNamespace
@ -357,26 +376,34 @@ const VariableEditor = (props: Props) => {
}
width={25}
value={query.namespace || null}
placeholder={requireNamespace ? undefined : 'Optional'}
placeholder={
requireNamespace ? undefined : t('components.variable-editor.placeholder-namespace', 'Optional')
}
/>
</Field>
)}
{hasRegion && (
<Field label="Region" data-testid={selectors.components.variableEditor.region.input}>
<Field
label={t('components.variable-editor.label-region', 'Region')}
data-testid={selectors.components.variableEditor.region.input}
>
<Select
aria-label="select region"
aria-label={t('components.variable-editor.aria-label-select-region', 'Select region')}
onChange={onChangeRegion}
options={regions.concat(variableOptionGroup)}
width={25}
value={query.region || null}
placeholder="Optional"
placeholder={t('components.variable-editor.placeholder-region', 'Optional')}
/>
</Field>
)}
{requireResource && (
<Field label="Resource" data-testid={selectors.components.variableEditor.resource.input}>
<Field
label={t('components.variable-editor.label-resource', 'Resource')}
data-testid={selectors.components.variableEditor.resource.input}
>
<Select
aria-label="select resource"
aria-label={t('components.variable-editor.aria-label-select-resource', 'Select resource')}
onChange={onChangeResource}
options={resources.concat(variableOptionGroup)}
width={25}
@ -385,9 +412,12 @@ const VariableEditor = (props: Props) => {
</Field>
)}
{requireCustomNamespace && (
<Field label={'Custom Namespace'} data-testid={selectors.components.variableEditor.customNamespace.input}>
<Field
label={t('components.variable-editor.label-custom-namespace', 'Custom Namespace')}
data-testid={selectors.components.variableEditor.customNamespace.input}
>
<Select
aria-label="select custom namespace"
aria-label={t('components.variable-editor.aria-label-select-custom-namespace', 'Select custom namespace')}
onChange={onChangeCustomNamespace}
options={
requireCustomNamespace
@ -396,7 +426,11 @@ const VariableEditor = (props: Props) => {
}
width={25}
value={query.customNamespace || null}
placeholder={requireCustomNamespace ? undefined : 'Optional'}
placeholder={
requireCustomNamespace
? undefined
: t('components.variable-editor.placeholder-custom-namespace', 'Optional')
}
/>
</Field>
)}
@ -413,7 +447,13 @@ const VariableEditor = (props: Props) => {
{errorMessage && (
<>
<Space v={2} />
<Alert severity="error" title="An error occurred while requesting metadata from Azure Monitor">
<Alert
severity="error"
title={t(
'components.variable-editor.title-error-occurred',
'An error occurred while requesting metadata from Azure Monitor'
)}
>
{errorMessage instanceof Error ? errorMessage.message : errorMessage}
</Alert>
</>

@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
import { useEffectOnce } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Select } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -21,6 +22,7 @@ const FormatAsField = ({
resultFormat,
}: FormatAsFieldProps) => {
const options = useMemo(() => [...formatOptions, variableOptionGroup], [variableOptionGroup, formatOptions]);
const { t } = useTranslate();
const handleChange = useCallback(
(change: SelectableValue<ResultFormat>) => {
@ -44,7 +46,10 @@ const FormatAsField = ({
});
return (
<Field label="Format as" data-testid={selectors.components.queryEditor.logsQueryEditor.formatSelection.input}>
<Field
label={t('components.format-as-field.label-format-as', 'Format as')}
data-testid={selectors.components.queryEditor.logsQueryEditor.formatSelection.input}
>
<Select
inputId={`${inputId}-format-as-field`}
value={resultFormat}

@ -0,0 +1,289 @@
{
"components": {
"advanced-multi": {
"label-advanced": "Advanced"
},
"advanced-resource-picker": {
"aria-label-add": "Add",
"aria-label-remove": "Remove",
"button-add-resource": "Add resource",
"button-add-resource-uri": "Add resource URI",
"label-namespace": "Namespace",
"label-region": "Region",
"label-resource-group": "Resource Group",
"label-resource-name": "Resource Name",
"label-resource-number": "Resource {{resourceNum}}",
"label-resource-uri": "Resource URI(s) ",
"label-subscription": "Subscription",
"placeholder-resource-name": "name",
"tooltip-region": "The code region of the resource. Optional for one resource but mandatory when selecting multiple ones.",
"tooltip-resource-uri": "Manually edit the <2>resource uri</2>. Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)"
},
"aggregate-item": {
"aria-label-aggregate-function": "Aggregate function",
"aria-label-column": "Column",
"aria-label-remove": "Remove",
"label-percentile": "OF"
},
"aggregate-section": {
"label-aggregate": "Aggregate",
"tooltip-aggregate": "Perform calculations across rows of data, such as count, sum, average, minimum, maximum, standard deviation or percentiles."
},
"aggregation-field": {
"label-aggregation": "Aggregation"
},
"app-registration-credentials": {
"aria-label-azure-cloud": "Azure Cloud",
"aria-label-client-id": "Client ID",
"aria-label-client-secret": "Client Secret",
"aria-label-symbol-client-secret": "Client Secret",
"aria-label-tenant-id": "Tenant ID",
"label-azure-cloud": "Azure Cloud",
"label-client-id": "Application (client) ID",
"label-client-secret": "Client Secret",
"label-symbol-client-secret": "Client Secret",
"label-tenant-id": "Directory (tenant) ID",
"placeholder-symbol-client-secret": "configured",
"reset-symbol-client-secret": "Reset"
},
"azure-cheat-sheet": {
"aria-label-raw-query": "{{queryDisplayName}} raw query",
"aria-label-use-query": "Use this query button",
"button-use-query": "Use this query",
"label-categories": "Categories",
"label-query-results": "Query results: {{numResults}}",
"placeholder-all-categories": "All categories",
"placeholder-search-logs": "Search Logs queries",
"text-loading": "Loading..."
},
"azure-cheat-sheet-modal": {
"aria-label-kick-start": "Kick start your query modal",
"title-kick-start": "Kick start your query"
},
"azure-credentials-form": {
"description-authentication": "Choose the type of authentication to Azure services",
"label-authentication": "Authentication",
"title-authentication": "Authentication"
},
"basic-logs-toggle": {
"aria-label-enable-basic-logs": "Basic Logs",
"description-basic-logs": "Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for <2>Basic Logs</2>.",
"label-enable-basic-logs": "Enable Basic Logs"
},
"config-editor": {
"description-additional-settings": "Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy.",
"title-additional-settings": "Additional settings"
},
"current-user-fallback-credentials": {
"alert-fallback-credentials-disabled": "Fallback credentials have been disabled. As user-based authentication only inherently supports requests with a user in scope, features such as alerting, recorded queries, or reporting will not function as expected. Please review the <2>documentation</2> for more details.",
"body-service-credentials": "User-based authentication does not inherently support Grafana features that make requests to the data source without a users details available to the request. An example of this is alerting. If you wish to ensure that features that do not have a user in the context of the request still function, please provide fallback credentials below.",
"description-authentication": "Choose the type of authentication to Azure services",
"description-service-credentials": "Choose if fallback service credentials are enabled or disabled for this data source",
"label-authentication": "Authentication",
"label-service-credentials": "Service Credentials",
"note-service-credentials": "Note: Features like alerting will be restricted to the access level of the fallback credentials rather than the user. This may present confusion for users and should be clarified.",
"title-fallback-credentials-disabled": "Fallback Credentials Disabled",
"title-fallback-service-credentials": "Fallback Service Credentials",
"title-service-credentials": "Service Credentials"
},
"default-subscription": {
"aria-label-default-subscription": "Default Subscription",
"label-default-subscription": "Default Subscription",
"load-subscriptions": "Load Subscriptions"
},
"dimension-fields": {
"aria-label-remove": "Remove",
"label-dimensions": "Dimensions",
"placeholder-field": "Field",
"placeholder-operation": "Operation",
"placeholder-select-value": "Select value",
"placeholder-select-values": "Select value(s)"
},
"editor-for-query-type": {
"body-unknown-query-type": "{{type}} was deprecated in Grafana 9. See the <3>deprecation notice</3> to get more information about how to migrate your queries. This is the current query definition:",
"title-unknown-query-type": "Unknown query type"
},
"filter": {
"aria-label-remove-filter": "Remove filter",
"placeholder-property": "Property",
"placeholder-value": "Value"
},
"filter-item": {
"aria-label-column": "Column",
"aria-label-column-value": "Column value",
"aria-label-operator": "Operator",
"label-or": "OR"
},
"filter-section": {
"label-add-group": "Add group",
"label-and": "AND",
"label-filters": "Filters",
"tooltip-filters": "Narrow results by applying conditions to specific columns."
},
"filters": {
"label-filters": "Filters"
},
"format-as-field": {
"label-format-as": "Format as"
},
"fuzzy-search": {
"aria-label-select-column": "Select Column",
"label-fuzzy-search": "Fuzzy Search",
"placeholder-search-team": "Enter search term",
"tooltip-fuzzy-search": "Find approximate text matches with tolerance for spelling variations. By default, fuzzy search scans all columns (*) in the entire table, not just specific fields."
},
"grafana-template-variable-fn-input": {
"label-grafana-template-variable": "Grafana template variable function",
"placeholder-grafana-template-variable": "Type a grafana template variable function, e.g. {{example}}"
},
"group-by-item": {
"aria-label-column": "Column",
"aria-label-remove": "Remove"
},
"group-by-section": {
"label-group-by": "Group by",
"tooltip-group-by": "Organize results into categories based on specified columns. Group by can be used independently to list unique values in selected columns, or combined with aggregate functions to produce summary statistics for each group. When used alone, it returns distinct combinations of the specified columns."
},
"kql-preview": {
"button-hide": "Hide",
"button-show": "Show",
"label-query-preview": "Query Preview"
},
"legend-format-field": {
"label-legend-format": "Legend format",
"placeholder-legend-format": "Alias patterns"
},
"limit-section": {
"label-limit": "Limit",
"placeholder-limit": "Enter limit",
"tooltip-limit": "Restrict the number of rows returned (default is 1000)."
},
"logs-management": {
"description-basic-logs-queries": "Basic Logs queries incur cost based on the amount of data scanned.",
"label-logs": "Logs",
"title-basic-logs-queries": "Basic Logs Queries",
"tooltip-logs": "Specifies whether to run a Basic or Analytics Logs query."
},
"logs-query-builder": {
"title-no-tables": "Resource loaded successfully but without any tables"
},
"logs-query-editor": {
"learn-more": "Learn More",
"view-query": "View query in Azure Portal"
},
"metric-name-field": {
"label-metric": "Metric"
},
"metric-namespace-field": {
"label-metric-namespace": "Metric namespace"
},
"nested-row": {
"text-loading": "Loading..."
},
"order-by-section": {
"aria-label-order-by-column": "Order by column",
"aria-label-order-direction": "Order Direction",
"label-by": "BY",
"label-order-by": "Order By",
"tooltip-order-by": "Sort results based on one or more columns in ascending or descending order."
},
"query-editor": {
"alert-error-occurred": "An error occurred while requesting metadata from Azure Monitor"
},
"query-header": {
"aria-label-kick-start": "Azure logs kick start your query button",
"button-kick-start-your-query": "Kick start your query",
"button-run-query": "Run query",
"label-service": "Service",
"placeholder-service": "Service...",
"title-switch-mode": "Switch editor mode?"
},
"raw-query": {
"aria-label-selector": "Selector"
},
"resource-field": {
"label-resource": "Resource",
"title-select-resource": "Select a resource"
},
"resource-label": {
"select-resource": "Select a resource"
},
"resource-picker": {
"button-apply": "Apply",
"button-cancel": "Cancel",
"header-location": "Location",
"header-scope": "Scope",
"header-type": "Type",
"heading-selection": "Selection",
"result-limit": "Showing first {{numResults}} results",
"text-loading": "Loading...",
"text-no-resources": "No resources found",
"title-error-occurred": "An error occurred while requesting resources from Azure Monitor"
},
"search": {
"aria-label-resource-search": "Resource search",
"placeholder-resource-search": "Search for a resource"
},
"subscription-field": {
"label-subscriptions": "Subscriptions",
"validation-subscriptions": "At least one subscription must be chosen."
},
"table-section": {
"aria-label-columns": "Columns",
"aria-label-table": "Table",
"label-columns": "Columns",
"label-table": "Table",
"placeholder-select-columns": "Select columns",
"placeholder-select-table": "Select a table"
},
"time-grain-field": {
"label-time-grain": "Time grain"
},
"time-management": {
"label-time-column": "Time Column",
"label-time-range": "Time-range",
"tooltip-time-column": "<0>Specifies the time column used for filtering. Defaults to the first tables <1>timeSpan</1> column, the first <3>datetime</3> column found or <5>TimeGenerated</5>.</0>",
"tooltip-time-range": "<0>Specifies the time-range used to query. The <1>Query</1> option will only use time-ranges specified in the query. <3>Dashboard</3> will only use the Grafana time-range. In Logs Builder mode, only Dashboard time will be used.</0>"
},
"top-field": {
"label-top": "Top"
},
"trace-type-field": {
"label-event-type": "Event Type",
"placeholder-event-type": "Choose event types"
},
"traces-query-editor": {
"label-operation-id": "Operation ID"
},
"user-auth-alert": {
"body-unsupported-auth": "Usage of this data source requires you to be authenticated via Azure Entra (formerly Azure Active Directory). Please review the <2>documentation</2> for more information.",
"title-unsupported-auth": "Unsupported authentication provider"
},
"user-auth-fallback-alert": {
"body-no-fallback-credentials": "Data source backend features (such as alerting) require service credentials to function. This data source is configured without service credential fallback, or the fallback functionality is disabled. Please review the <2>documentation</2> for more information.",
"title-no-fallback-credentials": "No fallback credentials available"
},
"variable-editor": {
"aria-label-select-custom-namespace": "Select custom namespace",
"aria-label-select-namespace": "Select namespace",
"aria-label-select-query-type": "Select query type",
"aria-label-select-region": "Select region",
"aria-label-select-resource": "Select resource",
"aria-label-select-resource-group": "Select resource group",
"aria-label-select-subscription": "Select subscription",
"label-custom-namespace": "Custom Namespace",
"label-namespace": "Namespace",
"label-query-type": "Query Type",
"label-region": "Region",
"label-resource": "Resource",
"label-resource-group": "Resource Group",
"label-resource-type": "Resource Type",
"label-subscription": "Subscription",
"placeholder-custom-namespace": "Optional",
"placeholder-namespace": "Optional",
"placeholder-region": "Optional",
"placeholder-resource-group": "Optional",
"title-error-occurred": "An error occurred while requesting metadata from Azure Monitor"
}
}
}

@ -0,0 +1,12 @@
module.exports = {
locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages
sort: true,
createOldCatalogs: false,
failOnWarnings: true,
verbose: false,
resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code
defaultNamespace: 'grafana-azure-monitor-datasource',
input: ['../**/*.{tsx,ts}'],
output: './locales/$LOCALE/$NAMESPACE.json',
};

@ -1,4 +1,5 @@
import { DataSourcePlugin, DashboardLoadedEvent } from '@grafana/data';
import { initPluginTranslations } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime';
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
@ -8,6 +9,8 @@ import pluginJson from './plugin.json';
import { trackAzureMonitorDashboardLoaded } from './tracking';
import { AzureMonitorQuery, AzureMonitorDataSourceJsonData, AzureQueryType, ResultFormat } from './types';
initPluginTranslations(pluginJson.id);
export const plugin = new DataSourcePlugin<Datasource, AzureMonitorQuery, AzureMonitorDataSourceJsonData>(Datasource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(AzureMonitorQueryEditor);

@ -6,6 +6,7 @@
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "12.1.0-pre",
"@grafana/i18n": "12.1.0-pre",
"@grafana/plugin-ui": "0.10.5",
"@grafana/runtime": "12.1.0-pre",
"@grafana/schema": "12.1.0-pre",
@ -37,6 +38,7 @@
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"i18next-parser": "9.3.0",
"react-select-event": "5.5.1",
"ts-node": "10.9.2",
"typescript": "5.7.3",
@ -48,7 +50,8 @@
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
"build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
"dev": "webpack -w -c ./webpack.config.ts --env development"
"dev": "webpack -w -c ./webpack.config.ts --env development",
"i18n-extract": "i18next --config locales/i18next-parser.config.cjs"
},
"packageManager": "yarn@4.6.0"
}

@ -133,5 +133,26 @@
"alerting": true,
"backend": true,
"logs": true,
"tracing": true
"tracing": true,
"languages": [
"en-US",
"fr-FR",
"es-ES",
"de-DE",
"pt-BR",
"zh-Hans",
"it-IT",
"ja-JP",
"id-ID",
"ko-KR",
"ru-RU",
"cs-CZ",
"nl-NL",
"hu-HU",
"pt-PT",
"pl-PL",
"sv-SE",
"tr-TR",
"zh-Hant"
]
}

@ -7,7 +7,7 @@ const config = async (env: Record<string, unknown>): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
return merge(baseConfig, {
externals: ['@kusto/monaco-kusto'],
externals: ['@kusto/monaco-kusto', 'i18next'],
});
};

@ -4524,12 +4524,6 @@
"time-zone-label": "Time zone",
"week-start-label": "Week start"
},
"time-regions": {
"advanced-description-cron": "Cron syntax",
"advanced-description-rest": " to define a recurrence schedule and duration",
"advanced-description-use": "Use ",
"advanced-label": "Advanced"
},
"variables": {
"title": "Variables"
},

@ -12,6 +12,7 @@ module.exports = {
input: [
'../../public/**/*.{tsx,ts}',
'!../../public/app/extensions/**/*', // Don't extract from Enterprise
'!../../public/app/plugins/**/*', // Don't extract from core plugins
'../../packages/grafana-ui/**/*.{tsx,ts}',
],
output: './public/locales/$LOCALE/$NAMESPACE.json',

@ -2487,6 +2487,7 @@ __metadata:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.1.0-pre"
"@grafana/e2e-selectors": "npm:12.1.0-pre"
"@grafana/i18n": "npm:12.1.0-pre"
"@grafana/plugin-configs": "npm:12.1.0-pre"
"@grafana/plugin-ui": "npm:0.10.5"
"@grafana/runtime": "npm:12.1.0-pre"
@ -2505,6 +2506,7 @@ __metadata:
"@types/react-dom": "npm:18.3.5"
fast-deep-equal: "npm:^3.1.3"
i18next: "npm:^24.0.0"
i18next-parser: "npm:9.3.0"
immer: "npm:10.1.1"
lodash: "npm:4.17.21"
monaco-editor: "npm:0.34.1"
@ -3169,7 +3171,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/i18n@workspace:*, @grafana/i18n@workspace:packages/grafana-i18n":
"@grafana/i18n@npm:12.1.0-pre, @grafana/i18n@workspace:*, @grafana/i18n@workspace:packages/grafana-i18n":
version: 0.0.0-use.local
resolution: "@grafana/i18n@workspace:packages/grafana-i18n"
dependencies:

Loading…
Cancel
Save