Internationalisation: Mark up MSSQL (#105532)

* scaffolding

* markup

* fix typos + extract translations

* update crowdin.yml

* uppercase Grafana
pull/105685/head
Ashley Harrison 3 days ago committed by GitHub
parent ea9040bbf7
commit b1c1d080e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/i18n-crowdin-download.yml
  2. 1
      .github/workflows/i18n-crowdin-upload.yml
  3. 6
      crowdin.yml
  4. 2
      eslint.config.js
  5. 2
      pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_process_client.go
  6. 76
      public/app/plugins/datasource/mssql/CheatSheet.tsx
  7. 102
      public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
  8. 209
      public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx
  9. 97
      public/app/plugins/datasource/mssql/configuration/Kerberos.tsx
  10. 128
      public/app/plugins/datasource/mssql/locales/en-US/mssql.json
  11. 12
      public/app/plugins/datasource/mssql/locales/i18next-parser.config.cjs
  12. 4
      public/app/plugins/datasource/mssql/module.ts
  13. 5
      public/app/plugins/datasource/mssql/package.json
  14. 23
      public/app/plugins/datasource/mssql/plugin.json
  15. 14
      public/app/plugins/datasource/mssql/webpack.config.ts
  16. 2
      yarn.lock

@ -152,12 +152,12 @@ jobs:
run: |
filesChanged="$(gh pr diff --name-only ${{ steps.crowdin-download.outputs.pull_request_url }})"
if [[ $(echo "$filesChanged" | grep -cv -e 'public/locales/[a-zA-Z\-]*/grafana.json' -e 'public/app/plugins/datasource/azuremonitor/locales/[a-zA-Z\-]*/grafana-azure-monitor-datasource.json') -ne 0 ]]; then
if [[ $(echo "$filesChanged" | grep -cv -e 'public/locales/[a-zA-Z\-]*/grafana.json' -e 'public/app/plugins/datasource/azuremonitor/locales/[a-zA-Z\-]*/grafana-azure-monitor-datasource.json' -e 'public/app/plugins/datasource/mssql/locales/[a-zA-Z\-]*/mssql.json') -ne 0 ]]; then
echo "Non-i18n changes detected, not approving"
exit 1
fi
if [[ $(echo "$filesChanged" | grep -c -e "public/locales/en-US" -e "public/app/plugins/datasource/azuremonitor/locales/en-US") -ne 0 ]]; then
if [[ $(echo "$filesChanged" | grep -c -e "public/locales/en-US" -e "public/app/plugins/datasource/azuremonitor/locales/en-US" -e "public/app/plugins/datasource/mssql/locales/en-US") -ne 0 ]]; then
echo "en-US changes detected, not approving"
exit 1
fi

@ -6,6 +6,7 @@ on:
paths:
- 'public/locales/en-US/grafana.json'
- 'public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json'
- 'public/app/plugins/datasource/mssql/locales/en-US/mssql.json'
branches:
- main

@ -15,4 +15,10 @@ files: [
"type": "i18next_json",
"dest": "plugins/azuremonitor/en-US/%original_file_name%"
},
{
"source": "public/app/plugins/datasource/mssql/locales/en-US/mssql.json",
"translation": "public/app/plugins/datasource/mssql/locales/%locale%/%original_file_name%",
"type": "i18next_json",
"dest": "plugins/mssql/en-US/%original_file_name%"
},
]

@ -19,7 +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'];
const pluginsToTranslate = ['public/app/plugins/datasource/azuremonitor', 'public/app/plugins/datasource/mssql'];
/**
* @type {Array<import('eslint').Linter.Config>}

@ -7,7 +7,7 @@ import (
mock "github.com/stretchr/testify/mock"
metadata "google.golang.org/grpc/metadata"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
export function CheatSheet() {
@ -8,29 +9,54 @@ export function CheatSheet() {
return (
<div>
<h2>MSSQL cheat sheet</h2>
Time series:
<h2>
<Trans i18nKey="cheat-sheet.title">MSSQL cheat sheet</Trans>
</h2>
<Trans i18nKey="cheat-sheet.time-series">Time series:</Trans>
<ul className={styles.ulPadding}>
<li>
return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the
macros below.
<Trans i18nKey="cheat-sheet.time-series-tip">
return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the
macros below.
</Trans>
</li>
<li>
<Trans i18nKey="cheat-sheet.time-series-tip-2">
any other columns returned will be the time point values.
</Trans>
</li>
<li>any other columns returned will be the time point values.</li>
</ul>
Optional:
<Trans i18nKey="cheat-sheet.optional">Optional:</Trans>
<ul className={styles.ulPadding}>
<li>
return column named <i>metric</i> to represent the series name.
<Trans i18nKey="cheat-sheet.optional-tip" values={{ columnName: 'metric' }}>
return column named <i>{'{{columnName}}'}</i> to represent the series name.
</Trans>
</li>
<li>
<Trans i18nKey="cheat-sheet.optional-tip-2" values={{ columnName: 'metric' }}>
If multiple value columns are returned the {'{{columnName}}'} column is used as prefix.
</Trans>
</li>
<li>
<Trans i18nKey="cheat-sheet.optional-tip-3" values={{ columnName: 'metric' }}>
If no column named {'{{columnName}}'} is found the column name of the value column is used as series name
</Trans>
</li>
<li>If multiple value columns are returned the metric column is used as prefix.</li>
<li>If no column named metric is found the column name of the value column is used as series name</li>
</ul>
<p>Resultsets of time series queries need to be sorted by time.</p>
Table:
<p>
<Trans i18nKey="cheat-sheet.resultsets-time-sorted">
Resultsets of time series queries need to be sorted by time.
</Trans>
</p>
<Trans i18nKey="cheat-sheet.table">Table:</Trans>
<ul className={styles.ulPadding}>
<li>return any set of columns</li>
<li>
<Trans i18nKey="cheat-sheet.table-tip">return any set of columns</Trans>
</li>
</ul>
Macros:
<Trans i18nKey="cheat-sheet.macros">Macros:</Trans>
{/* eslint-disable @grafana/no-untranslated-strings */}
<ul className={styles.ulPadding}>
<li>$__time(column) -&gt; column AS time</li>
<li>$__timeEpoch(column) -&gt; DATEDIFF(second, &apos;1970-01-01&apos;, column) AS time</li>
@ -44,9 +70,12 @@ export function CheatSheet() {
</li>
<li>
$__timeGroup(column, &apos;5m&apos;[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, &apos;1970-01-01&apos;,
column)/300.0, 0) as bigint)*300 by setting fillvalue grafana will fill in missing values according to the
interval fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen
value or NULL if none has been seen yet
column)/300.0, 0) as bigint)*300{' '}
<Trans i18nKey="cheat-sheet.fillvalue" values={{ null: 'NULL', previous: 'previous' }}>
by setting fillvalue Grafana will fill in missing values according to the interval. fillvalue can be either
a literal value, {'{{null}}'} or {'{{previous}}'}; {'{{previous}}'} will fill in the previous seen value or{' '}
{'{{null}}'} if none has been seen yet
</Trans>
</li>
<li>
$__timeGroupAlias(column, &apos;5m&apos;[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second,
@ -55,7 +84,13 @@ export function CheatSheet() {
<li>$__unixEpochGroup(column,&apos;5m&apos;) -&gt; FLOOR(column/300)*300</li>
<li>$__unixEpochGroupAlias(column,&apos;5m&apos;) -&gt; FLOOR(column/300)*300 AS [time]</li>
</ul>
<p>Example of group by and order by with $__timeGroup:</p>
{/* eslint-enable @grafana/no-untranslated-strings */}
<p>
<Trans i18nKey="cheat-sheet.example-time-group" values={{ timeGroupMacro: '$__timeGroup' }}>
Example of group by and order by with {'{{timeGroupMacro}}'}:
</Trans>
</p>
{/* eslint-disable @grafana/no-untranslated-strings */}
<pre>
<code>
SELECT $__timeGroup(date_time_col, &apos;1h&apos;) AS time, sum(value) as value <br />
@ -67,7 +102,11 @@ export function CheatSheet() {
<br />
</code>
</pre>
Or build your own conditionals using these macros which just return the values:
{/* eslint-enable @grafana/no-untranslated-strings */}
<Trans i18nKey="cheat-sheet.condtional-macros">
Or build your own conditionals using these macros which just return the values:
</Trans>
{/* eslint-disable @grafana/no-untranslated-strings */}
<ul className={styles.ulPadding}>
<li>$__timeFrom() -&gt; &apos;2017-04-21T05:01:17Z&apos;</li>
<li>$__timeTo() -&gt; &apos;2017-04-21T05:01:17Z&apos;</li>
@ -76,6 +115,7 @@ export function CheatSheet() {
<li>$__unixEpochNanoFrom() -&gt; 1494410783152415214</li>
<li>$__unixEpochNanoTo() -&gt; 1494497183142514872</li>
</ul>
{/* eslint-enable @grafana/no-untranslated-strings */}
</div>
);
}

@ -2,6 +2,7 @@ import { ChangeEvent } from 'react';
import { AzureCredentials, AzureAuthType } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Button, Field, Select, Input } from '@grafana/ui';
export interface Props {
@ -22,6 +23,7 @@ export const AzureCredentialsForm = (props: Props) => {
onCredentialsChange,
disabled,
} = props;
const { t } = useTranslate();
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
if (onCredentialsChange) {
@ -116,27 +118,30 @@ export const AzureCredentialsForm = (props: Props) => {
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{
value: 'clientsecret',
label: 'App Registration',
label: t('azureauth.azure-credentials-form.auth-options-app-registration', 'App Registration'),
},
];
if (managedIdentityEnabled) {
authTypeOptions.push({
value: 'msi',
label: 'Managed Identity',
label: t('azureauth.azure-credentials-form.auth-options-managed-identity', 'Managed Identity'),
});
}
if (azureEntraPasswordCredentialsEnabled) {
authTypeOptions.push({
value: 'ad-password',
label: 'Azure Entra Password',
label: t('azureauth.azure-credentials-form.auth-options-azure-entra', 'Azure Entra Password'),
});
}
return (
<div>
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
label={t('azureauth.azure-credentials-form.label-authentication', 'Authentication')}
description={t(
'azureauth.azure-credentials-form.description-authentication',
'Choose the type of authentication to Azure services'
)}
htmlFor="authentication-type"
>
<Select
@ -150,77 +155,94 @@ export const AzureCredentialsForm = (props: Props) => {
{credentials.authType === 'clientsecret' && (
<>
{azureCloudOptions && (
<Field label="Azure Cloud" htmlFor="azure-cloud-type" disabled={disabled}>
<Field
label={t('azureauth.azure-credentials-form.label-azure-cloud', 'Azure Cloud')}
htmlFor="azure-cloud-type"
disabled={disabled}
>
<Select
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions}
onChange={onAzureCloudChange}
isDisabled={disabled}
inputId="azure-cloud-type"
aria-label="Azure Cloud"
aria-label={t('azureauth.azure-credentials-form.aria-label-azure-cloud', 'Azure Cloud')}
width={20}
/>
</Field>
)}
<Field
label="Directory (tenant) ID"
label={t('azureauth.azure-credentials-form.label-tenant-id', 'Directory (tenant) ID')}
required
htmlFor="tenant-id"
invalid={!credentials.tenantId}
error={'Tenant ID is required'}
error={t('azureauth.azure-credentials-form.required-tenant-id', 'Tenant ID is required')}
>
<Input
width={45}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={onTenantIdChange}
disabled={disabled}
aria-label="Tenant ID"
aria-label={t('azureauth.azure-credentials-form.aria-label-tenant-id', 'Tenant ID')}
/>
</Field>
<Field
label="Application (client) ID"
label={t('azureauth.azure-credentials-form.label-client-id', 'Application (client) ID')}
required
htmlFor="client-id"
invalid={!credentials.clientId}
error={'Client ID is required'}
error={t('azureauth.azure-credentials-form.required-client-id', 'Client ID is required')}
>
<Input
width={45}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={onClientIdChange}
disabled={disabled}
aria-label="Client ID"
aria-label={t('azureauth.azure-credentials-form.arialabel-client-id', 'Client ID')}
/>
</Field>
{!disabled &&
(typeof credentials.clientSecret === 'symbol' ? (
<Field label="Client Secret" htmlFor="client-secret" required>
<Field
label={t('azureauth.azure-credentials-form.label-configured-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(
'azureauth.azure-credentials-form.aria-label-configured-client-secret',
'Client Secret'
)}
placeholder={t(
'azureauth.azure-credentials-form.placeholder-configured-client-secret',
'configured'
)}
disabled={true}
data-testid={'client-secret'}
width={45}
/>
<Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
Reset
<Trans i18nKey="azureauth.azure-credentials-form.client-secret-reset">Reset</Trans>
</Button>
</div>
</Field>
) : (
<Field
label="Client Secret"
label={t('azureauth.azure-credentials-form.label-client-secret', 'Client Secret')}
required
htmlFor="client-secret"
invalid={!credentials.clientSecret}
error={'Client secret is required'}
error={t('azureauth.azure-credentials-form.required-client-secret', 'Client secret is required')}
>
<Input
width={45}
aria-label="Client Secret"
aria-label={t('azureauth.azure-credentials-form.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}
@ -233,57 +255,73 @@ export const AzureCredentialsForm = (props: Props) => {
)}
{credentials.authType === 'ad-password' && azureEntraPasswordCredentialsEnabled && (
<>
<Field label="User Id" required htmlFor="user-id" invalid={!credentials.userId} error={'User ID is required'}>
<Field
label={t('azureauth.azure-credentials-form.label-user-id', 'User Id')}
required
htmlFor="user-id"
invalid={!credentials.userId}
error={'User ID is required'}
>
<Input
width={45}
value={credentials.userId || ''}
onChange={onUserIdChange}
disabled={disabled}
aria-label="User ID"
aria-label={t('azureauth.azure-credentials-form.aria-label-user-id', 'User ID')}
/>
</Field>
<Field
label="Application Client ID"
label={t('azureauth.azure-credentials-form.label-application-client-id', 'Application Client ID')}
required
htmlFor="application-client-id"
invalid={!credentials.clientId}
error={'Application Client ID is required'}
error={t(
'azureauth.azure-credentials-form.required-application-client-id',
'Application Client ID is required'
)}
>
<Input
width={45}
value={credentials.clientId || ''}
onChange={onClientIdChange}
disabled={disabled}
aria-label="Application Client ID"
aria-label={t(
'azureauth.azure-credentials-form.aria-label-application-client-id',
'Application Client ID'
)}
/>
</Field>
{!disabled &&
(typeof credentials.password === 'symbol' ? (
<Field label="Password" htmlFor="password" required>
<Field
label={t('azureauth.azure-credentials-form.label-password-configured', 'Password')}
htmlFor="password"
required
>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Input
aria-label="Password"
placeholder="configured"
aria-label={t('azureauth.azure-credentials-form.aria-label-password-configured', 'Password')}
placeholder={t('azureauth.azure-credentials-form.placeholder-password-configured', 'configured')}
disabled={true}
data-testid={'password'}
width={45}
/>
<Button variant="secondary" type="button" onClick={onPasswordReset} disabled={disabled}>
Reset
<Trans i18nKey="azureauth.azure-credentials-form.password-reset">Reset</Trans>
</Button>
</div>
</Field>
) : (
<Field
label="Password"
label={t('azureauth.azure-credentials-form.label-password', 'Password')}
required
htmlFor="password"
invalid={!credentials.password}
error={'Password is required'}
error={t('azureauth.azure-credentials-form.required-password', 'Password is required')}
>
<Input
width={45}
aria-label="Password"
aria-label={t('azureauth.azure-credentials-form.aria-label-password', 'Password')}
value={credentials.password || ''}
onChange={onPasswordChange}
id="password"

@ -10,6 +10,7 @@ import {
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { ConnectionLimits, useMigrateDatabaseFields } from '@grafana/sql';
@ -18,7 +19,7 @@ import {
Alert,
FieldSet,
Input,
Link,
TextLink,
SecretInput,
Select,
useStyles2,
@ -45,6 +46,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
useMigrateDatabaseFields(props);
const { options: dsSettings, onOptionsChange } = props;
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const jsonData = dsSettings.jsonData;
const azureAuthIsSupported = config.azureAuthEnabled;
@ -131,62 +133,98 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
docsLink="https://grafana.com/docs/grafana/latest/datasources/mssql/"
hasRequiredFields
/>
<Alert title="User Permission" severity="info">
The database user should only be granted SELECT permissions on the specified database and tables you want to
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect
against this we <em>highly</em> recommend you create a specific MS SQL user with restricted permissions. Check
out the{' '}
<Link rel="noreferrer" target="_blank" href="http://docs.grafana.org/features/datasources/mssql/">
Microsoft SQL Server Data Source Docs
</Link>{' '}
for more information.
<Alert title={t('configuration.configuration-editor.title-user-permission', 'User Permission')} severity="info">
<Trans
i18nKey="configuration.configuration-editor.body-user-permission"
values={{ permissionType: 'SELECT', example1: 'USE otherdb;', example2: 'DROP TABLE user;' }}
>
The database user should only be granted {'{{permissionType}}'} permissions on the specified database and
tables you want to query. Grafana does not validate that queries are safe so queries can contain any SQL
statement. For example, statements like <code>{'{{example1}}'}</code> and <code>{'{{example2}}'}</code> would
be executed. To protect against this we <em>highly</em> recommend you create a specific MS SQL user with
restricted permissions. Check out the{' '}
<TextLink external href="http://docs.grafana.org/features/datasources/mssql/">
Microsoft SQL Server Data Source Docs
</TextLink>{' '}
for more information.
</Trans>
</Alert>
<Divider />
<ConfigSection title="Connection">
<Field label="Host" required invalid={!dsSettings.url} error={'Host is required'}>
<ConfigSection title={t('configuration.configuration-editor.title-connection', 'Connection')}>
<Field
label={t('configuration.configuration-editor.title-host', 'Host')}
required
invalid={!dsSettings.url}
error={t('configuration.configuration-editor.required-host', 'Host is required')}
>
<Input
width={LONG_WIDTH}
name="host"
type="text"
value={dsSettings.url || ''}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="localhost:1433"
onChange={onDSOptionChanged('url')}
/>
</Field>
<Field label="Database" required invalid={!jsonData.database} error={'Database is required'}>
<Field
label={t('configuration.configuration-editor.title-database', 'Database')}
required
invalid={!jsonData.database}
error={t('configuration.configuration-editor.required-database', 'Database is required')}
>
<Input
width={LONG_WIDTH}
name="database"
value={jsonData.database || ''}
placeholder="database name"
placeholder={t('configuration.configuration-editor.placeholder-database', 'database name')}
onChange={onUpdateDatasourceJsonDataOption(props, 'database')}
/>
</Field>
</ConfigSection>
<ConfigSection title="TLS/SSL Auth">
<ConfigSection title={t('configuration.configuration-editor.title-tls-auth', 'TLS/SSL Auth')}>
<Field
htmlFor="encrypt"
description={
<>
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server.
<Trans i18nKey="configuration.configuration-editor.description-encrypt">
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server.
</Trans>
<ul className={styles.ulPadding}>
<li>
<i>disable</i> - Data sent between client and server is not encrypted.
<Trans
i18nKey="configuration.configuration-editor.description-encrypt-disable"
values={{ encryptionValue: 'disable' }}
>
<i>{'{{encryptionValue}}'}</i> - Data sent between client and server is not encrypted.
</Trans>
</li>
<li>
<i>false</i> - Data sent between client and server is not encrypted beyond the login packet. (default)
<Trans
i18nKey="configuration.configuration-editor.description-encrypt-false"
values={{ encryptionValue: 'false' }}
>
<i>{'{{encryptionValue}}'}</i> - Data sent between client and server is not encrypted beyond the
login packet. (default)
</Trans>
</li>
<li>
<i>true</i> - Data sent between client and server is encrypted.
<Trans
i18nKey="configuration.configuration-editor.description-encrypt-true"
values={{ encryptionValue: 'true' }}
>
<i>{'{{encryptionValue}}'}</i> - Data sent between client and server is encrypted.
</Trans>
</li>
</ul>
If you&apos;re using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable
encryption to be able to connect.
<Trans i18nKey="configuration.configuration-editor.description-encrypt-older-version">
If you&apos;re using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to
disable encryption to be able to connect.
</Trans>
</>
}
label="Encrypt"
label={t('configuration.configuration-editor.label-encrypt', 'Encrypt')}
>
<Select
options={encryptOptions}
@ -199,7 +237,10 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
{jsonData.encrypt === MSSQLEncryptOptions.true ? (
<>
<Field htmlFor="skipTlsVerify" label="Skip TLS Verify">
<Field
htmlFor="skipTlsVerify"
label={t('configuration.configuration-editor.label-skip-tls', 'Skip TLS Verify')}
>
<Switch id="skipTlsVerify" onChange={onSkipTLSVerifyChanged} value={jsonData.tlsSkipVerify || false} />
</Field>
{jsonData.tlsSkipVerify ? null : (
@ -207,22 +248,32 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
<Field
description={
<span>
Path to file containing the public key certificate of the CA that signed the SQL Server
certificate. Needed when the server certificate is self signed.
<Trans i18nKey="configuration.configuration-editor.description-tls-cert">
Path to file containing the public key certificate of the CA that signed the SQL Server
certificate. Needed when the server certificate is self signed.
</Trans>
</span>
}
label="TLS/SSL Root Certificate"
label={t('configuration.configuration-editor.label-tls-cert', 'TLS/SSL Root Certificate')}
>
<Input
value={jsonData.sslRootCertFile || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')}
placeholder="TLS/SSL root certificate file path"
placeholder={t(
'configuration.configuration-editor.placeholder-tls-cert',
'TLS/SSL root certificate file path'
)}
width={LONG_WIDTH}
/>
</Field>
<Field label="Hostname in server certificate">
<Field
label={t('configuration.configuration-editor.label-common-name', 'Hostname in server certificate')}
>
<Input
placeholder="Common Name (CN) in server certificate"
placeholder={t(
'configuration.configuration-editor.placeholder-common-name',
'Common Name (CN) in server certificate'
)}
value={jsonData.serverName || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'serverName')}
width={LONG_WIDTH}
@ -234,40 +285,54 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
) : null}
</ConfigSection>
<ConfigSection title="Authentication">
<ConfigSection title={t('configuration.configuration-editor.title-authentication', 'Authentication')}>
<Field
label="Authentication Type"
label={t('configuration.configuration-editor.label-auth-type', 'Authentication Type')}
htmlFor="authenticationType"
description={
<ul className={styles.ulPadding}>
<li>
<i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the
SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-sql-server">
<i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the
SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
</Trans>
</li>
<li>
<i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already
logged onto Windows and have enabled this option for MS SQL Server.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-windows-auth">
<i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already
logged onto Windows and have enabled this option for MS SQL Server.
</Trans>
</li>
{azureAuthIsSupported && (
<li>
<i>Azure Authentication</i> Securely authenticate and access Azure resources and applications using
Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-azure-auth">
<i>Azure Authentication</i> Securely authenticate and access Azure resources and applications using
Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.
</Trans>
</li>
)}
<li>
<i>Windows AD: Username + password</i> Windows Active Directory - Sign on for domain user via
username/password.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-username-password">
<i>Windows AD: Username + password</i> Windows Active Directory - Sign on for domain user via
username/password.
</Trans>
</li>
<li>
<i>Windows AD: Keytab</i> Windows Active Directory - Sign on for domain user via keytab file.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-keytab">
<i>Windows AD: Keytab</i> Windows Active Directory - Sign on for domain user via keytab file.
</Trans>
</li>
<li>
<i>Windows AD: Credential cache</i> Windows Active Directory - Sign on for domain user via credential
cache.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-credential-cache">
<i>Windows AD: Credential cache</i> Windows Active Directory - Sign on for domain user via credential
cache.
</Trans>
</li>
<li>
<i>Windows AD: Credential cache file</i> Windows Active Directory - Sign on for domain user via
credential cache file.
<Trans i18nKey="configuration.configuration-editor.description-auth-type-credential-cache-file">
<i>Windows AD: Credential cache file</i> Windows Active Directory - Sign on for domain user via
credential cache file.
</Trans>
</li>
</ul>
}
@ -292,30 +357,33 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
!jsonData.authenticationType) && (
<>
<Field
label="Username"
label={t('configuration.configuration-editor.label-username', 'Username')}
required
invalid={!dsSettings.user}
error={'Username is required'}
error={t('configuration.configuration-editor.required-username', 'Username is required')}
description={jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw ? UsernameMessage : ''}
>
<Input
value={dsSettings.user || ''}
placeholder={
jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw ? 'name@EXAMPLE.COM' : 'user'
jsonData.authenticationType === MSSQLAuthenticationType.kerberosRaw
? // eslint-disable-next-line @grafana/no-untranslated-strings
'name@EXAMPLE.COM'
: t('configuration.configuration-editor.placeholder-user', 'user')
}
onChange={onDSOptionChanged('user')}
width={LONG_WIDTH}
/>
</Field>
<Field
label="Password"
label={t('configuration.configuration-editor.label-password', 'Password')}
required
invalid={!dsSettings.secureJsonFields.password && !dsSettings.secureJsonData?.password}
error={'Password is required'}
error={t('configuration.configuration-editor.required-password', 'Password is required')}
>
<SecretInput
width={LONG_WIDTH}
placeholder="Password"
placeholder={t('configuration.configuration-editor.placeholder-password', 'Password')}
isConfigured={dsSettings.secureJsonFields && dsSettings.secureJsonFields.password}
onReset={onResetPassword}
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
@ -326,7 +394,9 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
)}
{azureAuthIsSupported && jsonData.authenticationType === MSSQLAuthenticationType.azureAuth && (
<FieldSet label="Azure Authentication Settings">
<FieldSet
label={t('configuration.configuration-editor.label-auth-settings', 'Azure Authentication Settings')}
>
<azureAuthSettings.azureAuthSettingsUI dataSourceConfig={dsSettings} onChange={onOptionsChange} />
</FieldSet>
)}
@ -334,25 +404,37 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
<Divider />
<ConfigSection
title="Additional settings"
description="Additional settings are optional settings that can be configured for more control over your data source. This includes connection limits, connection timeout, group-by time interval, and Secure Socks Proxy."
title={t('configuration.configuration-editor.title-additional-settings', 'Additional settings')}
description={t(
'configuration.configuration-editor.description-additional-settings',
'Additional settings are optional settings that can be configured for more control over your data source. This includes connection limits, connection timeout, group-by time interval, and Secure Socks Proxy.'
)}
isCollapsible={true}
isInitiallyOpen={true}
>
<ConnectionLimits options={dsSettings} onOptionsChange={onOptionsChange} />
<ConfigSubSection title="Connection details">
<ConfigSubSection
title={t('configuration.configuration-editor.title-connection-details', 'Connection details')}
>
<Field
description={
<span>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example
<code>1m</code> if your data is written every minute.
<Trans
i18nKey="configuration.configuration-editor.description-min-interval"
values={{ exampleInterval: '1m' }}
>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for
example
<code>{'{{exampleInterval}}'}</code> if your data is written every minute.
</Trans>
</span>
}
label="Min time interval"
label={t('configuration.configuration-editor.label-min-interval', 'Min time interval')}
>
<Input
width={LONG_WIDTH}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="1m"
value={jsonData.timeInterval || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
@ -361,11 +443,16 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
<Field
description={
<span>
The number of seconds to wait before canceling the request when connecting to the database. The default
is <code>0</code>, meaning no timeout.
<Trans
i18nKey="configuration.configuration-editor.description-connection-timeout"
values={{ defaultTimeout: '0' }}
>
The number of seconds to wait before canceling the request when connecting to the database. The
default is <code>{'{{defaultTimeout}}'}</code>, meaning no timeout.
</Trans>
</span>
}
label="Connection timeout"
label={t('configuration.configuration-editor.label-connection-timeout', 'Connection timeout')}
>
<NumberInput
width={LONG_WIDTH}

@ -1,19 +1,23 @@
import { SyntheticEvent } from 'react';
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { ConfigSubSection } from '@grafana/plugin-ui';
import { FieldSet, Input, Field } from '@grafana/ui';
import { FieldSet, Input, Field, TextLink } from '@grafana/ui';
import { MSSQLAuthenticationType, MssqlOptions } from '../types';
export const UsernameMessage = (
<span>
Use the format <code>user@EXAMPLE.COM</code>. Realm is derived from the username.
<Trans i18nKey="configuration.kerberos.username-message">
Use the format <code>user@EXAMPLE.COM</code>. Realm is derived from the username.
</Trans>
</span>
);
export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => {
const { options: settings, onOptionsChange } = props;
const { t } = useTranslate();
const jsonData = settings.jsonData;
const LONG_WIDTH = 40;
@ -36,23 +40,29 @@ export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOp
return (
<>
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosKeytab && (
<FieldSet label="Windows AD: Keytab">
<FieldSet label={t('configuration.kerberos-config.label-keytab', 'Windows AD: Keytab')}>
<Field
label="Username"
label={t('configuration.kerberos-config.label-username', 'Username')}
required
invalid={!settings.user}
error={'Username is required'}
error={t('configuration.kerberos-config.required-username', 'Username is required')}
description={UsernameMessage}
>
<Input
value={settings.user || ''}
placeholder="name@EXAMPLE.COM"
placeholder={t('configuration.kerberos-config.placeholder-username', 'name@EXAMPLE.COM')}
onChange={(e) => onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })}
width={LONG_WIDTH}
/>
</Field>
<Field label="Keytab file path" required invalid={!keytabFilePath} error={'Keytab file path is required'}>
<Field
label={t('configuration.kerberos-config.label-keytab-file-path', 'Keytab file path')}
required
invalid={!keytabFilePath}
error={'Keytab file path is required'}
>
<Input
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="/home/grot/grot.keytab"
onChange={onKeytabFileChanged}
width={LONG_WIDTH}
@ -64,14 +74,18 @@ export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOp
)}
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCache && (
<FieldSet label="Windows AD: Credential cache">
<FieldSet label={t('configuration.kerberos-config.label-credential-cache', 'Windows AD: Credential cache')}>
<Field
label="Credential cache path"
label={t('configuration.kerberos-config.label-credential-cache-path', 'Credential cache path')}
required
invalid={!credentialCache}
error={'Credential cache path is required'}
error={t(
'configuration.kerberos-config.required-credential-cache-path',
'Credential cache path is required'
)}
>
<Input
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="/tmp/krb5cc_1000"
onChange={onCredentialCacheChanged}
width={LONG_WIDTH}
@ -83,28 +97,34 @@ export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOp
)}
{jsonData.authenticationType === MSSQLAuthenticationType.kerberosCredentialCacheLookupFile && (
<FieldSet label="Windows AD: Credential cache file">
<FieldSet
label={t('configuration.kerberos-config.label-credential-cache-file', 'Windows AD: Credential cache file')}
>
<Field
label="Username"
label={t('configuration.kerberos-config.label-username', 'Username')}
required
invalid={!settings.user}
error={'Username is required'}
error={t('configuration.kerberos-config.required-username', 'Username is required')}
description={UsernameMessage}
>
<Input
value={settings.user || ''}
placeholder="name@EXAMPLE.COM"
placeholder={t('configuration.kerberos-config.placeholder-username', 'name@EXAMPLE.COM')}
onChange={(e) => onOptionsChange({ ...settings, ...{ ['user']: e.currentTarget.value } })}
width={LONG_WIDTH}
/>
</Field>
<Field
label="Credential cache file path"
label={t('configuration.kerberos-config.label-credential-cache-file-path', 'Credential cache file path')}
required
invalid={!credentialCacheLookupFile}
error={'Credential cache file path is required'}
error={t(
'configuration.kerberos-config.required-credential-cache-file-path',
'Credential cache file path is required'
)}
>
<Input
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="/home/grot/cache.json"
onChange={onCredentialCacheFileChanged}
width={LONG_WIDTH}
@ -120,6 +140,7 @@ export const KerberosConfig = (props: DataSourcePluginOptionsEditorProps<MssqlOp
export const KerberosAdvancedSettings = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => {
const { options: settings } = props;
const { t } = useTranslate();
const jsonData = settings.jsonData;
const configFilePath = jsonData?.configFilePath;
const LONG_WIDTH = 40;
@ -137,19 +158,28 @@ export const KerberosAdvancedSettings = (props: DataSourcePluginOptionsEditorPro
return (
<>
<ConfigSubSection title="Windows AD: Advanced Settings">
<ConfigSubSection
title={t('configuration.kerberos-advanced-settings.title-advanced-settings', 'Windows AD: Advanced Settings')}
>
<FieldSet>
<Field
label="UDP Preference Limit"
label={t('configuration.kerberos-advanced-settings.label-udp-preference-limit', 'UDP Preference Limit')}
// TODO
description={
<span>
The default is <code>1</code> and means always use TCP and is optional.
<Trans
i18nKey="configuration.kerberos-advanced-settings.description-udp-preference-limit"
values={{ default: '1' }}
>
The default is <code>{'{{default}}'}</code> and means always use TCP and is optional.
</Trans>
</span>
}
>
<Input
type="text"
width={LONG_WIDTH}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="0"
defaultValue={jsonData.UDPConnectionLimit}
onChange={(e) => {
@ -161,31 +191,42 @@ export const KerberosAdvancedSettings = (props: DataSourcePluginOptionsEditorPro
/>
</Field>
<Field
label="DNS Lookup KDC"
label={t('configuration.kerberos-advanced-settings.label-dns-lookup-kdc', 'DNS Lookup KDC')}
description={
<span>
Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm. The
default is <code>true</code>.
<Trans
i18nKey="configuration.kerberos-advanced-settings.description-dns-lookup-kdc"
values={{ default: 'true' }}
>
Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm.
The default is <code>{'{{default}}'}</code>.
</Trans>
</span>
}
>
<Input
type="text"
width={LONG_WIDTH}
// eslint-disable-next-line @grafana/no-untranslated-strings
placeholder="true"
defaultValue={jsonData.enableDNSLookupKDC}
onChange={onDNSLookupKDCChanged}
/>
</Field>
<Field
label="krb5 config file path"
label={t('configuration.kerberos-advanced-settings.label-krb5-config-file-path', 'krb5 config file path')}
description={
<span>
The path to the configuration file for the{' '}
<a href="https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html">
MIT krb5 package
</a>
. The default is <code>/etc/krb5.conf</code>.
<Trans
i18nKey="configuration.kerberos-advanced-settings.description-krb5-config-file-path"
values={{ default: '/etc/krb5.conf' }}
>
The path to the configuration file for the{' '}
<TextLink external href="https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html">
MIT krb5 package
</TextLink>
. The default is <code>{'{{default}}'}</code>.
</Trans>
</span>
}
>

@ -0,0 +1,128 @@
{
"azureauth": {
"azure-credentials-form": {
"aria-label-application-client-id": "Application Client ID",
"aria-label-azure-cloud": "Azure Cloud",
"aria-label-client-secret": "Client Secret",
"aria-label-configured-client-secret": "Client Secret",
"aria-label-password": "Password",
"aria-label-password-configured": "Password",
"aria-label-tenant-id": "Tenant ID",
"aria-label-user-id": "User ID",
"arialabel-client-id": "Client ID",
"auth-options-app-registration": "App Registration",
"auth-options-azure-entra": "Azure Entra Password",
"auth-options-managed-identity": "Managed Identity",
"client-secret-reset": "Reset",
"description-authentication": "Choose the type of authentication to Azure services",
"label-application-client-id": "Application Client ID",
"label-authentication": "Authentication",
"label-azure-cloud": "Azure Cloud",
"label-client-id": "Application (client) ID",
"label-client-secret": "Client Secret",
"label-configured-client-secret": "Client Secret",
"label-password": "Password",
"label-password-configured": "Password",
"label-tenant-id": "Directory (tenant) ID",
"label-user-id": "User Id",
"password-reset": "Reset",
"placeholder-configured-client-secret": "configured",
"placeholder-password-configured": "configured",
"required-application-client-id": "Application Client ID is required",
"required-client-id": "Client ID is required",
"required-client-secret": "Client secret is required",
"required-password": "Password is required",
"required-tenant-id": "Tenant ID is required"
}
},
"cheat-sheet": {
"condtional-macros": "Or build your own conditionals using these macros which just return the values:",
"example-time-group": "Example of group by and order by with {{timeGroupMacro}}:",
"fillvalue": "by setting fillvalue Grafana will fill in missing values according to the interval. fillvalue can be either a literal value, {{null}} or {{previous}}; {{previous}} will fill in the previous seen value or {{null}} if none has been seen yet",
"macros": "Macros:",
"optional": "Optional:",
"optional-tip": "return column named <1>{{columnName}}</1> to represent the series name.",
"optional-tip-2": "If multiple value columns are returned the {{columnName}} column is used as prefix.",
"optional-tip-3": "If no column named {{columnName}} is found the column name of the value column is used as series name",
"resultsets-time-sorted": "Resultsets of time series queries need to be sorted by time.",
"table": "Table:",
"table-tip": "return any set of columns",
"time-series": "Time series:",
"time-series-tip": "return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.",
"time-series-tip-2": "any other columns returned will be the time point values.",
"title": "MSSQL cheat sheet"
},
"configuration": {
"configuration-editor": {
"body-user-permission": "The database user should only be granted {{permissionType}} permissions on the specified database and tables you want to query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements like <3>{{example1}}</3> and <5>{{example2}}</5> would be executed. To protect against this we <7>highly</7> recommend you create a specific MS SQL user with restricted permissions. Check out the <10>Microsoft SQL Server Data Source Docs</10> for more information.",
"description-additional-settings": "Additional settings are optional settings that can be configured for more control over your data source. This includes connection limits, connection timeout, group-by time interval, and Secure Socks Proxy.",
"description-auth-type-azure-auth": "<0>Azure Authentication</0> Securely authenticate and access Azure resources and applications using Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.",
"description-auth-type-credential-cache": "<0>Windows AD: Credential cache</0> Windows Active Directory - Sign on for domain user via credential cache.",
"description-auth-type-credential-cache-file": "<0>Windows AD: Credential cache file</0> Windows Active Directory - Sign on for domain user via credential cache file.",
"description-auth-type-keytab": "<0>Windows AD: Keytab</0> Windows Active Directory - Sign on for domain user via keytab file.",
"description-auth-type-sql-server": "<0>SQL Server Authentication</0> This is the default mechanism to connect to MS SQL Server. Enter the SQL Server Authentication login or the Windows Authentication login in the DOMAIN\\User format.",
"description-auth-type-username-password": "<0>Windows AD: Username + password</0> Windows Active Directory - Sign on for domain user via username/password.",
"description-auth-type-windows-auth": "<0>Windows Authentication</0> Windows Integrated Security - single sign on for users who are already logged onto Windows and have enabled this option for MS SQL Server.",
"description-connection-timeout": "The number of seconds to wait before canceling the request when connecting to the database. The default is <1>{{defaultTimeout}}</1>, meaning no timeout.",
"description-encrypt": "Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server.",
"description-encrypt-disable": "<0>{{encryptionValue}}</0> - Data sent between client and server is not encrypted.",
"description-encrypt-false": "<0>{{encryptionValue}}</0> - Data sent between client and server is not encrypted beyond the login packet. (default)",
"description-encrypt-older-version": "If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.",
"description-encrypt-true": "<0>{{encryptionValue}}</0> - Data sent between client and server is encrypted.",
"description-min-interval": "A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example<1>{{exampleInterval}}</1> if your data is written every minute.",
"description-tls-cert": "Path to file containing the public key certificate of the CA that signed the SQL Server certificate. Needed when the server certificate is self signed.",
"label-auth-settings": "Azure Authentication Settings",
"label-auth-type": "Authentication Type",
"label-common-name": "Hostname in server certificate",
"label-connection-timeout": "Connection timeout",
"label-encrypt": "Encrypt",
"label-min-interval": "Min time interval",
"label-password": "Password",
"label-skip-tls": "Skip TLS Verify",
"label-tls-cert": "TLS/SSL Root Certificate",
"label-username": "Username",
"placeholder-common-name": "Common Name (CN) in server certificate",
"placeholder-database": "database name",
"placeholder-password": "Password",
"placeholder-tls-cert": "TLS/SSL root certificate file path",
"placeholder-user": "user",
"required-database": "Database is required",
"required-host": "Host is required",
"required-password": "Password is required",
"required-username": "Username is required",
"title-additional-settings": "Additional settings",
"title-authentication": "Authentication",
"title-connection": "Connection",
"title-connection-details": "Connection details",
"title-database": "Database",
"title-host": "Host",
"title-tls-auth": "TLS/SSL Auth",
"title-user-permission": "User Permission"
},
"kerberos": {
"username-message": "Use the format <1>user@EXAMPLE.COM</1>. Realm is derived from the username."
},
"kerberos-advanced-settings": {
"description-dns-lookup-kdc": "Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm. The default is <1>{{default}}</1>.",
"description-krb5-config-file-path": "The path to the configuration file for the <2>MIT krb5 package</2>. The default is <4>{{default}}</4>.",
"description-udp-preference-limit": "The default is <1>{{default}}</1> and means always use TCP and is optional.",
"label-dns-lookup-kdc": "DNS Lookup KDC",
"label-krb5-config-file-path": "krb5 config file path",
"label-udp-preference-limit": "UDP Preference Limit",
"title-advanced-settings": "Windows AD: Advanced Settings"
},
"kerberos-config": {
"label-credential-cache": "Windows AD: Credential cache",
"label-credential-cache-file": "Windows AD: Credential cache file",
"label-credential-cache-file-path": "Credential cache file path",
"label-credential-cache-path": "Credential cache path",
"label-keytab": "Windows AD: Keytab",
"label-keytab-file-path": "Keytab file path",
"label-username": "Username",
"placeholder-username": "name@EXAMPLE.COM",
"required-credential-cache-file-path": "Credential cache file path is required",
"required-credential-cache-path": "Credential cache path is required",
"required-username": "Username is required"
}
}
}

@ -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: 'mssql',
input: ['../**/*.{tsx,ts}'],
output: './locales/$LOCALE/$NAMESPACE.json',
};

@ -1,11 +1,15 @@
import { DataSourcePlugin } from '@grafana/data';
import { initPluginTranslations } from '@grafana/i18n';
import { SQLQuery, SqlQueryEditorLazy } from '@grafana/sql';
import { CheatSheet } from './CheatSheet';
import { ConfigurationEditor } from './configuration/ConfigurationEditor';
import { MssqlDatasource } from './datasource';
import pluginJson from './plugin.json';
import { MssqlOptions } from './types';
initPluginTranslations(pluginJson.id);
export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery, MssqlOptions>(MssqlDatasource)
.setQueryEditor(SqlQueryEditorLazy)
.setQueryEditorHelp(CheatSheet)

@ -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/sql": "12.1.0-pre",
@ -25,6 +26,7 @@
"@types/lodash": "4.17.15",
"@types/node": "22.12.0",
"@types/react": "18.3.18",
"i18next-parser": "9.3.0",
"ts-node": "10.9.2",
"typescript": "5.7.3",
"webpack": "5.97.1"
@ -35,7 +37,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"
}

@ -35,5 +35,26 @@
"queryOptions": {
"minInterval": 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"
]
}

@ -1,4 +1,14 @@
import config from '@grafana/plugin-configs/webpack.config';
import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import grafanaConfig from '@grafana/plugin-configs/webpack.config';
const config = async (env: Record<string, unknown>): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
return merge(baseConfig, {
externals: ['i18next'],
});
};
// eslint-disable-next-line no-barrel-files/no-barrel-files
export default config;

@ -2685,6 +2685,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"
@ -2697,6 +2698,7 @@ __metadata:
"@types/lodash": "npm:4.17.15"
"@types/node": "npm:22.12.0"
"@types/react": "npm:18.3.18"
i18next-parser: "npm:9.3.0"
lodash: "npm:4.17.21"
react: "npm:18.3.1"
rxjs: "npm:7.8.1"

Loading…
Cancel
Save