mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
Cloudwatch: Remove awsDatasourcesNewFormStyling feature toggle (#90128)
* Remove toggle from cloudwatch plugin * Remove feature toggle from registry --------- Co-authored-by: Kevin Yu <kevinwcyu@users.noreply.github.com>
This commit is contained in:
parent
b7379b7b51
commit
5b17cd93c5
@ -7885,9 +7885,6 @@ exports[`no gf-form usage`] = {
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx:5381": [
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx:5381": [
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
|
@ -51,7 +51,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes |
|
||||
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes |
|
||||
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes |
|
||||
| `awsDatasourcesNewFormStyling` | Applies new form styling for configuration and query editors in AWS plugins | Yes |
|
||||
| `managedPluginsInstall` | Install managed plugins directly from plugins catalog | Yes |
|
||||
| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes |
|
||||
| `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes |
|
||||
|
@ -122,7 +122,6 @@ export interface FeatureToggles {
|
||||
recoveryThreshold?: boolean;
|
||||
lokiStructuredMetadata?: boolean;
|
||||
teamHttpHeaders?: boolean;
|
||||
awsDatasourcesNewFormStyling?: boolean;
|
||||
cachingOptimizeSerializationMemoryUsage?: boolean;
|
||||
panelTitleSearchInV1?: boolean;
|
||||
managedPluginsInstall?: boolean;
|
||||
|
@ -785,14 +785,6 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: identityAccessTeam,
|
||||
},
|
||||
{
|
||||
Name: "awsDatasourcesNewFormStyling",
|
||||
Description: "Applies new form styling for configuration and query editors in AWS plugins",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true",
|
||||
FrontendOnly: true,
|
||||
Owner: awsDatasourcesSquad,
|
||||
},
|
||||
{
|
||||
Name: "cachingOptimizeSerializationMemoryUsage",
|
||||
Description: "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.",
|
||||
|
@ -103,7 +103,6 @@ cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false
|
||||
recoveryThreshold,GA,@grafana/alerting-squad,false,true,false
|
||||
lokiStructuredMetadata,GA,@grafana/observability-logs,false,false,false
|
||||
teamHttpHeaders,preview,@grafana/identity-access-team,false,false,false
|
||||
awsDatasourcesNewFormStyling,GA,@grafana/aws-datasources,false,false,true
|
||||
cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
panelTitleSearchInV1,experimental,@grafana/search-and-storage,true,false,false
|
||||
managedPluginsInstall,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
|
|
@ -423,10 +423,6 @@ const (
|
||||
// Enables Team LBAC for datasources to apply team headers to the client requests
|
||||
FlagTeamHttpHeaders = "teamHttpHeaders"
|
||||
|
||||
// FlagAwsDatasourcesNewFormStyling
|
||||
// Applies new form styling for configuration and query editors in AWS plugins
|
||||
FlagAwsDatasourcesNewFormStyling = "awsDatasourcesNewFormStyling"
|
||||
|
||||
// FlagCachingOptimizeSerializationMemoryUsage
|
||||
// If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.
|
||||
FlagCachingOptimizeSerializationMemoryUsage = "cachingOptimizeSerializationMemoryUsage"
|
||||
|
@ -469,6 +469,7 @@
|
||||
"name": "awsDatasourcesNewFormStyling",
|
||||
"resourceVersion": "1720021873452",
|
||||
"creationTimestamp": "2023-10-12T08:59:10Z",
|
||||
"deletionTimestamp": "2024-07-05T10:20:55Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC"
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { AwsAuthType } from '@grafana/aws-sdk';
|
||||
import { PluginContextProvider, PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
CloudWatchSettings,
|
||||
@ -130,258 +129,229 @@ describe('Render', () => {
|
||||
datasource.getVariables = jest.fn().mockReturnValue([]);
|
||||
});
|
||||
|
||||
const originalFormFeatureToggleValue = config.featureToggles.awsDatasourcesNewFormStyling;
|
||||
|
||||
const cleanupFeatureToggle = () => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = originalFormFeatureToggleValue;
|
||||
};
|
||||
|
||||
function run() {
|
||||
it('it should disable access key id field when the datasource has been previously configured', async () => {
|
||||
setup({
|
||||
secureJsonFields: {
|
||||
secretKey: true,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByPlaceholderText('Configured')).toBeDisabled());
|
||||
it('it should disable access key id field when the datasource has been previously configured', async () => {
|
||||
setup({
|
||||
secureJsonFields: {
|
||||
secretKey: true,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByPlaceholderText('Configured')).toBeDisabled());
|
||||
});
|
||||
|
||||
it('should show credentials profile name field', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText('Credentials Profile Name')).toBeInTheDocument());
|
||||
it('should show credentials profile name field', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText('Credentials Profile Name')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should show a warning if `credentials` auth type is used without a profile or database configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: undefined,
|
||||
database: undefined,
|
||||
},
|
||||
});
|
||||
await waitFor(async () =>
|
||||
expect(screen.getByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).toBeInTheDocument()
|
||||
);
|
||||
it('should show a warning if `credentials` auth type is used without a profile or database configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: undefined,
|
||||
database: undefined,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should not show a warning if `credentials` auth type is used and a profile is configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: 'profile',
|
||||
database: undefined,
|
||||
},
|
||||
});
|
||||
await waitFor(async () =>
|
||||
expect(screen.queryByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).not.toBeInTheDocument()
|
||||
);
|
||||
it('should not show a warning if `credentials` auth type is used and a profile is configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: 'profile',
|
||||
database: undefined,
|
||||
},
|
||||
});
|
||||
await waitFor(async () =>
|
||||
expect(screen.queryByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show a warning if `credentials` auth type is used and a database is configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: undefined,
|
||||
database: 'database',
|
||||
},
|
||||
});
|
||||
await waitFor(async () =>
|
||||
expect(screen.queryByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).not.toBeInTheDocument()
|
||||
);
|
||||
it('should not show a warning if `credentials` auth type is used and a database is configured', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Credentials,
|
||||
profile: undefined,
|
||||
database: 'database',
|
||||
},
|
||||
});
|
||||
await waitFor(async () =>
|
||||
expect(screen.queryByText(CREDENTIALS_AUTHENTICATION_WARNING_MESSAGE)).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should show access key and secret access key fields when the datasource has not been configured before', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Keys,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByLabelText('Access Key ID')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Secret Access Key')).toBeInTheDocument();
|
||||
});
|
||||
it('should show access key and secret access key fields when the datasource has not been configured before', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.Keys,
|
||||
},
|
||||
});
|
||||
|
||||
it('should show arn role field', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.ARN,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText('Assume Role ARN')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should display namespace field', async () => {
|
||||
setup();
|
||||
await waitFor(async () => expect(screen.getByText('Namespaces of Custom Metrics')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should show a deprecation warning if `arn` auth type is used', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.ARN,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText(ARN_DEPRECATION_WARNING_MESSAGE)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should display log group selector field', async () => {
|
||||
setup();
|
||||
await waitFor(async () => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should only display the first two default log groups and show all of them when clicking "Show all" button', async () => {
|
||||
setup({
|
||||
version: 2,
|
||||
jsonData: {
|
||||
logGroups: [
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-foo:*', name: 'logGroup-foo' },
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-bar:*', name: 'logGroup-bar' },
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-baz:*', name: 'logGroup-baz' },
|
||||
],
|
||||
},
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(await screen.getByText('logGroup-foo')).toBeInTheDocument();
|
||||
expect(await screen.getByText('logGroup-bar')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('logGroup-baz')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Show all'));
|
||||
|
||||
expect(await screen.getByText('logGroup-baz')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should load the data source if it was saved before', async () => {
|
||||
const SAVED_VERSION = 2;
|
||||
setup({ version: SAVED_VERSION });
|
||||
await waitFor(async () => expect(loadDataSourceMock).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should not load the data source if it wasnt saved before', async () => {
|
||||
const SAVED_VERSION = undefined;
|
||||
setup({ version: SAVED_VERSION });
|
||||
await waitFor(async () => expect(loadDataSourceMock).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should show error message if Select log group button is clicked when data source is never saved', async () => {
|
||||
setup({ version: 1 });
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('You need to save the data source before adding log groups.')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error message if Select log group button is clicked when data source is saved before but have unsaved changes', async () => {
|
||||
const SAVED_VERSION = 3;
|
||||
const newProps = {
|
||||
...props,
|
||||
options: {
|
||||
...props.options,
|
||||
version: SAVED_VERSION,
|
||||
},
|
||||
};
|
||||
const meta: PluginMeta = {
|
||||
...newProps.options,
|
||||
id: 'cloudwatch',
|
||||
type: PluginType.datasource,
|
||||
info: {} as PluginMetaInfo,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...newProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
const rerenderProps = {
|
||||
...newProps,
|
||||
options: {
|
||||
...newProps.options,
|
||||
jsonData: {
|
||||
...newProps.options.jsonData,
|
||||
authType: AwsAuthType.Default,
|
||||
},
|
||||
},
|
||||
};
|
||||
rerender(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...rerenderProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('AWS SDK Default')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(
|
||||
'You have unsaved connection detail changes. You need to save the data source before adding log groups.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should open log group selector if Select log group button is clicked when data source has saved changes', async () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
options: {
|
||||
...props.options,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
const meta: PluginMeta = {
|
||||
...newProps.options,
|
||||
id: 'cloudwatch',
|
||||
type: PluginType.datasource,
|
||||
info: {} as PluginMetaInfo,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
const { rerender } = render(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...newProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
const rerenderProps = {
|
||||
...newProps,
|
||||
options: {
|
||||
...newProps.options,
|
||||
version: 2,
|
||||
},
|
||||
};
|
||||
rerender(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...rerenderProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument());
|
||||
});
|
||||
}
|
||||
|
||||
describe('QueryEditor with awsDatasourcesNewFormStyling feature toggle enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = false;
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupFeatureToggle();
|
||||
});
|
||||
run();
|
||||
describe('QueryEditor with awsDatasourcesNewFormStyling feature toggle enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = true;
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupFeatureToggle();
|
||||
});
|
||||
run();
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByLabelText('Access Key ID')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Secret Access Key')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show arn role field', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.ARN,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText('Assume Role ARN')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should display namespace field', async () => {
|
||||
setup();
|
||||
await waitFor(async () => expect(screen.getByText('Namespaces of Custom Metrics')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should show a deprecation warning if `arn` auth type is used', async () => {
|
||||
setup({
|
||||
jsonData: {
|
||||
authType: AwsAuthType.ARN,
|
||||
},
|
||||
});
|
||||
await waitFor(async () => expect(screen.getByText(ARN_DEPRECATION_WARNING_MESSAGE)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should display log group selector field', async () => {
|
||||
setup();
|
||||
await waitFor(async () => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should only display the first two default log groups and show all of them when clicking "Show all" button', async () => {
|
||||
setup({
|
||||
version: 2,
|
||||
jsonData: {
|
||||
logGroups: [
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-foo:*', name: 'logGroup-foo' },
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-bar:*', name: 'logGroup-bar' },
|
||||
{ arn: 'arn:aws:logs:us-east-2:123456789012:log-group:logGroup-baz:*', name: 'logGroup-baz' },
|
||||
],
|
||||
},
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(await screen.getByText('logGroup-foo')).toBeInTheDocument();
|
||||
expect(await screen.getByText('logGroup-bar')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('logGroup-baz')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Show all'));
|
||||
|
||||
expect(await screen.getByText('logGroup-baz')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should load the data source if it was saved before', async () => {
|
||||
const SAVED_VERSION = 2;
|
||||
setup({ version: SAVED_VERSION });
|
||||
await waitFor(async () => expect(loadDataSourceMock).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should not load the data source if it wasnt saved before', async () => {
|
||||
const SAVED_VERSION = undefined;
|
||||
setup({ version: SAVED_VERSION });
|
||||
await waitFor(async () => expect(loadDataSourceMock).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should show error message if Select log group button is clicked when data source is never saved', async () => {
|
||||
setup({ version: 1 });
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('You need to save the data source before adding log groups.')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error message if Select log group button is clicked when data source is saved before but have unsaved changes', async () => {
|
||||
const SAVED_VERSION = 3;
|
||||
const newProps = {
|
||||
...props,
|
||||
options: {
|
||||
...props.options,
|
||||
version: SAVED_VERSION,
|
||||
},
|
||||
};
|
||||
const meta: PluginMeta = {
|
||||
...newProps.options,
|
||||
id: 'cloudwatch',
|
||||
type: PluginType.datasource,
|
||||
info: {} as PluginMetaInfo,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...newProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
const rerenderProps = {
|
||||
...newProps,
|
||||
options: {
|
||||
...newProps.options,
|
||||
jsonData: {
|
||||
...newProps.options.jsonData,
|
||||
authType: AwsAuthType.Default,
|
||||
},
|
||||
},
|
||||
};
|
||||
rerender(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...rerenderProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('AWS SDK Default')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(
|
||||
'You have unsaved connection detail changes. You need to save the data source before adding log groups.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should open log group selector if Select log group button is clicked when data source has saved changes', async () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
options: {
|
||||
...props.options,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
const meta: PluginMeta = {
|
||||
...newProps.options,
|
||||
id: 'cloudwatch',
|
||||
type: PluginType.datasource,
|
||||
info: {} as PluginMetaInfo,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
const { rerender } = render(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...newProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument());
|
||||
const rerenderProps = {
|
||||
...newProps,
|
||||
options: {
|
||||
...newProps.options,
|
||||
version: 2,
|
||||
},
|
||||
};
|
||||
rerender(
|
||||
<PluginContextProvider meta={meta}>
|
||||
<ConfigEditor {...rerenderProps} />
|
||||
</PluginContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByText('Select log groups'));
|
||||
await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
@ -9,10 +10,11 @@ import {
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
DataSourceTestSucceeded,
|
||||
DataSourceTestFailed,
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
import { ConfigSection } from '@grafana/experimental';
|
||||
import { getAppEvents, usePluginInteractionReporter, getDataSourceSrv, config } from '@grafana/runtime';
|
||||
import { Alert, Input, InlineField, FieldProps, SecureSocksProxySettings, Field, Divider } from '@grafana/ui';
|
||||
import { Alert, Input, FieldProps, Field, Divider, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import { SelectableResourceValue } from '../../resources/types';
|
||||
@ -42,7 +44,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
const [logGroupFieldState, setLogGroupFieldState] = useState<LogGroupFieldState>({
|
||||
invalid: false,
|
||||
});
|
||||
const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling;
|
||||
|
||||
useEffect(() => setLogGroupFieldState({ invalid: false }), [props.options]);
|
||||
const report = usePluginInteractionReporter();
|
||||
useEffect(() => {
|
||||
@ -83,8 +85,10 @@ export const ConfigEditor = (props: Props) => {
|
||||
}
|
||||
}, [options.jsonData.authType, options.jsonData.database, options.jsonData.profile]);
|
||||
|
||||
return newFormStylingEnabled ? (
|
||||
<div className="width-30">
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.formStyles}>
|
||||
{warning && (
|
||||
<Alert title="CloudWatch Authentication" severity="warning" onRemove={dismissWarning}>
|
||||
{warning}
|
||||
@ -192,116 +196,6 @@ export const ConfigEditor = (props: Props) => {
|
||||
datasourceUid={options.jsonData.tracingDatasourceUid}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{warning && (
|
||||
<Alert title="CloudWatch Authentication" severity="warning" onRemove={dismissWarning}>
|
||||
{warning}
|
||||
</Alert>
|
||||
)}
|
||||
<ConnectionConfig
|
||||
{...props}
|
||||
labelWidth={29}
|
||||
loadRegions={
|
||||
datasource &&
|
||||
(async () => {
|
||||
return datasource.resources
|
||||
.getRegions()
|
||||
.then((regions) =>
|
||||
regions.reduce(
|
||||
(acc: string[], curr: SelectableResourceValue) => (curr.value ? [...acc, curr.value] : acc),
|
||||
[]
|
||||
)
|
||||
);
|
||||
})
|
||||
}
|
||||
externalId={externalId}
|
||||
>
|
||||
<InlineField label="Namespaces of Custom Metrics" labelWidth={29} tooltip="Namespaces of Custom Metrics.">
|
||||
<Input
|
||||
width={60}
|
||||
placeholder="Namespace1,Namespace2"
|
||||
value={options.jsonData.customMetricsNamespaces || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'customMetricsNamespaces')}
|
||||
/>
|
||||
</InlineField>
|
||||
</ConnectionConfig>
|
||||
|
||||
{config.secureSocksDSProxyEnabled && (
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
|
||||
<h3 className="page-heading">CloudWatch Logs</h3>
|
||||
<div className="gf-form-group">
|
||||
<InlineField
|
||||
label="Query Result Timeout"
|
||||
labelWidth={28}
|
||||
tooltip='Grafana will poll for Cloudwatch Logs query results every second until Done status is returned from AWS or timeout is exceeded, in which case Grafana will return an error. The default period is 30 minutes. Note: For Alerting, the timeout defined in the config file will take precedence. Must be a valid duration string, such as "15m" "30s" "2000ms" etc.'
|
||||
invalid={Boolean(logsTimeoutError)}
|
||||
>
|
||||
<Input
|
||||
width={60}
|
||||
placeholder="30m"
|
||||
value={options.jsonData.logsTimeout || ''}
|
||||
onChange={onUpdateDatasourceJsonDataOption(props, 'logsTimeout')}
|
||||
title={'The timeout must be a valid duration string, such as "15m" "30s" "2000ms" etc.'}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Default Log Groups"
|
||||
labelWidth={28}
|
||||
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
|
||||
shrink={true}
|
||||
{...logGroupFieldState}
|
||||
>
|
||||
{datasource ? (
|
||||
<LogGroupsFieldWrapper
|
||||
region={defaultRegion ?? ''}
|
||||
datasource={datasource}
|
||||
onBeforeOpen={() => {
|
||||
if (saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
let error = 'You need to save the data source before adding log groups.';
|
||||
if (props.options.version && props.options.version > 1) {
|
||||
error =
|
||||
'You have unsaved connection detail changes. You need to save the data source before adding log groups.';
|
||||
}
|
||||
setLogGroupFieldState({
|
||||
invalid: true,
|
||||
error,
|
||||
});
|
||||
throw new Error(error);
|
||||
}}
|
||||
legacyLogGroupNames={defaultLogGroups}
|
||||
logGroups={logGroups}
|
||||
onChange={(updatedLogGroups) => {
|
||||
onOptionsChange({
|
||||
...props.options,
|
||||
jsonData: {
|
||||
...props.options.jsonData,
|
||||
logGroups: updatedLogGroups,
|
||||
defaultLogGroups: undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
maxNoOfVisibleLogGroups={2}
|
||||
//legacy props
|
||||
legacyOnChange={(logGroups) => {
|
||||
updateDatasourcePluginJsonDataOption(props, 'defaultLogGroups', logGroups);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</InlineField>
|
||||
</div>
|
||||
<XrayLinkConfig
|
||||
onChange={(uid) => updateDatasourcePluginJsonDataOption(props, 'tracingDatasourceUid', uid)}
|
||||
datasourceUid={options.jsonData.tracingDatasourceUid}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -367,3 +261,9 @@ function useDataSourceSavedState(props: Props) {
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formStyles: css({
|
||||
maxWidth: theme.spacing(50),
|
||||
}),
|
||||
});
|
||||
|
@ -2,8 +2,6 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
|
||||
import { GetDimensionKeysRequest } from '../../resources/types';
|
||||
import { VariableQueryType } from '../../types';
|
||||
@ -24,12 +22,6 @@ const defaultQuery = {
|
||||
|
||||
const ds = setupMockedDataSource();
|
||||
|
||||
const originalFormFeatureToggleValue = config.featureToggles.awsDatasourcesNewFormStyling;
|
||||
|
||||
const cleanupFeatureToggle = () => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = originalFormFeatureToggleValue;
|
||||
};
|
||||
|
||||
ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([
|
||||
{ label: 'a1', value: 'a1' },
|
||||
{ label: 'b1', value: 'b1' },
|
||||
@ -82,221 +74,200 @@ describe('VariableEditor', () => {
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
});
|
||||
function run() {
|
||||
describe('and a new variable is created', () => {
|
||||
it('should trigger a query using the first query type in the array', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = defaultQuery;
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
describe('and a new variable is created', () => {
|
||||
it('should trigger a query using the first query type in the array', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = defaultQuery;
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Regions')).toBeInTheDocument();
|
||||
// Should not render any fields besides Query Type
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and an existing variable is edited', () => {
|
||||
it('should trigger new query using the saved query type', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.Metrics,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Metrics')).toBeInTheDocument();
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('a1')).toBeInTheDocument();
|
||||
const namespaceSelect = screen.queryByRole('combobox', { name: 'Namespace' });
|
||||
expect(namespaceSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('z2')).toBeInTheDocument();
|
||||
// Should only render Query Type, Region, and Namespace selectors
|
||||
const metricSelect = screen.queryByRole('combobox', { name: 'Metric' });
|
||||
expect(metricSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should parse dimensionFilters correctly', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { s4: 'foo' },
|
||||
};
|
||||
await act(async () => {
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
});
|
||||
const filterItem = screen.getByTestId('cloudwatch-dimensions-filter-item');
|
||||
expect(filterItem).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('s4')).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('foo')).toBeInTheDocument();
|
||||
|
||||
// change filter key
|
||||
const keySelect = screen.getByRole('combobox', { name: 'Dimensions filter key' });
|
||||
// confirms getDimensionKeys was called with filter and that the element uses keysForDimensionFilter
|
||||
select(keySelect, 'v4', {
|
||||
container: document.body,
|
||||
});
|
||||
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith(
|
||||
{
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionFilters: undefined,
|
||||
},
|
||||
false
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { v4: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
// set filter value
|
||||
const valueSelect = screen.getByRole('combobox', { name: 'Dimensions filter value' });
|
||||
await select(valueSelect, 'bar', {
|
||||
container: document.body,
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { v4: 'bar' },
|
||||
});
|
||||
});
|
||||
it('should parse multiFilters correctly', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar'] },
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue('Tags.blah')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const filterItem = screen.getByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItem).toBeInTheDocument();
|
||||
expect(within(filterItem).getByDisplayValue('foo, bar')).toBeInTheDocument();
|
||||
|
||||
// set filter value
|
||||
const valueElement = screen.getByTestId('cloudwatch-multifilter-item-value');
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
await userEvent.type(valueElement!, ',baz');
|
||||
fireEvent.blur(valueElement!);
|
||||
|
||||
expect(screen.getByDisplayValue('foo, bar, baz')).toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar', 'baz'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and a different region is selected', () => {
|
||||
it('should clear invalid fields', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { s4: 'foo' },
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
const querySelect = screen.queryByLabelText('Query type');
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
|
||||
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
|
||||
await waitFor(() =>
|
||||
select(regionSelect, 'b1', {
|
||||
container: document.body,
|
||||
})
|
||||
);
|
||||
expect(screen.queryByText('Regions')).toBeInTheDocument();
|
||||
// Should not render any fields besides Query Type
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(ds.datasource.resources.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
|
||||
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
|
||||
expect(props.onChange).toHaveBeenCalledWith({
|
||||
describe('and an existing variable is edited', () => {
|
||||
it('should trigger new query using the saved query type', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.Metrics,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Metrics')).toBeInTheDocument();
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('a1')).toBeInTheDocument();
|
||||
const namespaceSelect = screen.queryByRole('combobox', { name: 'Namespace' });
|
||||
expect(namespaceSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('z2')).toBeInTheDocument();
|
||||
// Should only render Query Type, Region, and Namespace selectors
|
||||
const metricSelect = screen.queryByRole('combobox', { name: 'Metric' });
|
||||
expect(metricSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should parse dimensionFilters correctly', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { s4: 'foo' },
|
||||
};
|
||||
await act(async () => {
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
});
|
||||
const filterItem = screen.getByTestId('cloudwatch-dimensions-filter-item');
|
||||
expect(filterItem).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('s4')).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('foo')).toBeInTheDocument();
|
||||
|
||||
// change filter key
|
||||
const keySelect = screen.getByRole('combobox', { name: 'Dimensions filter key' });
|
||||
// confirms getDimensionKeys was called with filter and that the element uses keysForDimensionFilter
|
||||
select(keySelect, 'v4', {
|
||||
container: document.body,
|
||||
});
|
||||
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith(
|
||||
{
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionFilters: undefined,
|
||||
},
|
||||
false
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
refId: 'CloudWatchVariableQueryEditor-VariableQuery',
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'b1',
|
||||
// metricName i3 exists in the new region and should not be removed
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
// dimensionKey s4 and valueDimension do not exist in the new region and should be removed
|
||||
dimensionKey: '',
|
||||
dimensionFilters: {},
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { v4: undefined },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('LogGroups queryType is selected', () => {
|
||||
it('should only render region and prefix', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.LogGroups,
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByLabelText('Log group prefix');
|
||||
screen.getByLabelText('Region');
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument();
|
||||
// set filter value
|
||||
const valueSelect = screen.getByRole('combobox', { name: 'Dimensions filter value' });
|
||||
await select(valueSelect, 'bar', {
|
||||
container: document.body,
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { v4: 'bar' },
|
||||
});
|
||||
});
|
||||
}
|
||||
it('should parse multiFilters correctly', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar'] },
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
describe('variable editor with awsDatasourcesNewFormStyling feature toggle enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = false;
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupFeatureToggle();
|
||||
});
|
||||
run();
|
||||
describe('variable editor with awsDatasourcesNewFormStyling feature toggle enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.awsDatasourcesNewFormStyling = true;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue('Tags.blah')).toBeInTheDocument();
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupFeatureToggle();
|
||||
|
||||
const filterItem = screen.getByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItem).toBeInTheDocument();
|
||||
expect(within(filterItem).getByDisplayValue('foo, bar')).toBeInTheDocument();
|
||||
|
||||
// set filter value
|
||||
const valueElement = screen.getByTestId('cloudwatch-multifilter-item-value');
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
await userEvent.type(valueElement!, ',baz');
|
||||
fireEvent.blur(valueElement!);
|
||||
|
||||
expect(screen.getByDisplayValue('foo, bar, baz')).toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar', 'baz'] },
|
||||
});
|
||||
run();
|
||||
});
|
||||
});
|
||||
describe('and a different region is selected', () => {
|
||||
it('should clear invalid fields', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
dimensionFilters: { s4: 'foo' },
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
const querySelect = screen.queryByLabelText('Query type');
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
|
||||
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
|
||||
await waitFor(() =>
|
||||
select(regionSelect, 'b1', {
|
||||
container: document.body,
|
||||
})
|
||||
);
|
||||
|
||||
expect(ds.datasource.resources.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
|
||||
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
|
||||
expect(props.onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
refId: 'CloudWatchVariableQueryEditor-VariableQuery',
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'b1',
|
||||
// metricName i3 exists in the new region and should not be removed
|
||||
metricName: 'i3',
|
||||
// dimensionKey s4 and valueDimension do not exist in the new region and should be removed
|
||||
dimensionKey: '',
|
||||
dimensionFilters: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('LogGroups queryType is selected', () => {
|
||||
it('should only render region and prefix', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.LogGroups,
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByLabelText('Log group prefix');
|
||||
screen.getByLabelText('Region');
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { EditorField } from '@grafana/experimental';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineField } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import {
|
||||
@ -50,7 +52,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
const accountState = useAccountOptions(datasource.resources, query.region);
|
||||
const dimensionKeyError = useEnsureVariableHasSingleSelection(datasource, dimensionKey);
|
||||
|
||||
const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling;
|
||||
const onRegionChange = async (region: string) => {
|
||||
const validatedQuery = await sanitizeQuery({
|
||||
...parsedQuery,
|
||||
@ -119,8 +120,11 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
VariableQueryType.DimensionKeys,
|
||||
VariableQueryType.DimensionValues,
|
||||
].includes(parsedQuery.queryType);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={newFormStylingEnabled ? 'width-15' : ''}>
|
||||
<div className={styles.formStyles}>
|
||||
<VariableQueryField
|
||||
value={parsedQuery.queryType}
|
||||
options={queryTypes}
|
||||
@ -129,7 +133,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
}
|
||||
label="Query type"
|
||||
inputId={`variable-query-type-${query.refId}`}
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
{hasRegionField && (
|
||||
<VariableQueryField
|
||||
@ -139,7 +142,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
label="Region"
|
||||
isLoading={regionIsLoading}
|
||||
inputId={`variable-query-region-${query.refId}`}
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
)}
|
||||
{hasAccountIDField &&
|
||||
@ -152,7 +154,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
onChange={(accountId?: string) => onQueryChange({ ...parsedQuery, accountId })}
|
||||
options={[ALL_ACCOUNTS_OPTION, ...accountState?.value]}
|
||||
allowCustomValue={false}
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
)}
|
||||
{hasNamespaceField && (
|
||||
@ -163,7 +164,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
label="Namespace"
|
||||
inputId={`variable-query-namespace-${query.refId}`}
|
||||
allowCustomValue
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.DimensionValues && (
|
||||
@ -175,7 +175,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
label="Metric"
|
||||
inputId={`variable-query-metric-${query.refId}`}
|
||||
allowCustomValue
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
<VariableQueryField
|
||||
value={dimensionKey || null}
|
||||
@ -184,37 +183,22 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
label="Dimension key"
|
||||
inputId={`variable-query-dimension-key-${query.refId}`}
|
||||
allowCustomValue
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
error={dimensionKeyError}
|
||||
/>
|
||||
{newFormStylingEnabled ? (
|
||||
<EditorField label="Dimensions" className="width-30" tooltip="Dimensions to filter the returned values on">
|
||||
<Dimensions
|
||||
metricStat={{ ...parsedQuery, dimensions: parsedQuery.dimensionFilters }}
|
||||
onChange={(dimensions) => {
|
||||
onChange({ ...parsedQuery, dimensionFilters: dimensions });
|
||||
}}
|
||||
disableExpressions={true}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
) : (
|
||||
<InlineField
|
||||
label="Dimensions"
|
||||
labelWidth={20}
|
||||
shrink
|
||||
tooltip="Dimensions to filter the returned values on"
|
||||
>
|
||||
<Dimensions
|
||||
metricStat={{ ...parsedQuery, dimensions: parsedQuery.dimensionFilters }}
|
||||
onChange={(dimensions) => {
|
||||
onChange({ ...parsedQuery, dimensionFilters: dimensions });
|
||||
}}
|
||||
disableExpressions={true}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<EditorField
|
||||
label="Dimensions"
|
||||
className={styles.dimensionsWidth}
|
||||
tooltip="Dimensions to filter the returned values on"
|
||||
>
|
||||
<Dimensions
|
||||
metricStat={{ ...parsedQuery, dimensions: parsedQuery.dimensionFilters }}
|
||||
onChange={(dimensions) => {
|
||||
onChange({ ...parsedQuery, dimensionFilters: dimensions });
|
||||
}}
|
||||
disableExpressions={true}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && (
|
||||
@ -223,7 +207,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
placeholder="i-XXXXXXXXXXXXXXXXX"
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, instanceID: value })}
|
||||
label="Instance ID"
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.EC2InstanceAttributes && (
|
||||
@ -233,7 +216,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
|
||||
label="Attribute name"
|
||||
interactive={true}
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
tooltip={
|
||||
<>
|
||||
{'Attribute or tag to query on. Tags should be formatted "Tags.<name>". '}
|
||||
@ -247,60 +229,31 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{newFormStylingEnabled ? (
|
||||
<EditorField
|
||||
label="Filters"
|
||||
tooltipInteractive
|
||||
tooltip={
|
||||
<>
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Pre-defined ec2:DescribeInstances filters/tags
|
||||
</a>
|
||||
{' and the values to filter on. Tags should be formatted tag:<name>.'}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MultiFilter
|
||||
filters={parsedQuery.ec2Filters ?? {}}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, ec2Filters: filters });
|
||||
}}
|
||||
keyPlaceholder="filter/tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
) : (
|
||||
<InlineField
|
||||
label="Filters"
|
||||
labelWidth={20}
|
||||
shrink
|
||||
tooltip={
|
||||
<>
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Pre-defined ec2:DescribeInstances filters/tags
|
||||
</a>
|
||||
{' and the values to filter on. Tags should be formatted tag:<name>.'}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MultiFilter
|
||||
filters={parsedQuery.ec2Filters ?? {}}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, ec2Filters: filters });
|
||||
}}
|
||||
keyPlaceholder="filter/tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<EditorField
|
||||
label="Filters"
|
||||
tooltipInteractive
|
||||
tooltip={
|
||||
<>
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Pre-defined ec2:DescribeInstances filters/tags
|
||||
</a>
|
||||
{' and the values to filter on. Tags should be formatted tag:<name>.'}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MultiFilter
|
||||
filters={parsedQuery.ec2Filters ?? {}}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, ec2Filters: filters });
|
||||
}}
|
||||
keyPlaceholder="filter/tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.ResourceArns && (
|
||||
@ -309,31 +262,17 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
value={parsedQuery.resourceType}
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, resourceType: value })}
|
||||
label="Resource type"
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
{newFormStylingEnabled ? (
|
||||
<EditorField label="Tags" tooltip="Tags to filter the returned values on.">
|
||||
<MultiFilter
|
||||
filters={parsedQuery.tags}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, tags: filters });
|
||||
}}
|
||||
keyPlaceholder="tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
) : (
|
||||
<InlineField label="Tags" shrink labelWidth={20} tooltip="Tags to filter the returned values on.">
|
||||
<MultiFilter
|
||||
filters={parsedQuery.tags}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, tags: filters });
|
||||
}}
|
||||
keyPlaceholder="tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<EditorField label="Tags" tooltip="Tags to filter the returned values on.">
|
||||
<MultiFilter
|
||||
filters={parsedQuery.tags}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, tags: filters });
|
||||
}}
|
||||
keyPlaceholder="tag"
|
||||
datasource={datasource}
|
||||
/>
|
||||
</EditorField>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.LogGroups && (
|
||||
@ -341,9 +280,17 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
value={query.logGroupPrefix ?? ''}
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, logGroupPrefix: value })}
|
||||
label="Log group prefix"
|
||||
newFormStylingEnabled={newFormStylingEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formStyles: css({
|
||||
maxWidth: theme.spacing(30),
|
||||
}),
|
||||
dimensionsWidth: css({
|
||||
width: theme.spacing(50),
|
||||
}),
|
||||
});
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorField } from '@grafana/experimental';
|
||||
import { Alert, InlineField, Select, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Select } from '@grafana/ui';
|
||||
|
||||
import { VariableQueryType } from '../../types';
|
||||
import { removeMarginBottom } from '../styles';
|
||||
|
||||
const LABEL_WIDTH = 20;
|
||||
|
||||
interface VariableQueryFieldProps<T> {
|
||||
onChange: (value: T) => void;
|
||||
options: SelectableValue[];
|
||||
@ -17,7 +13,6 @@ interface VariableQueryFieldProps<T> {
|
||||
inputId?: string;
|
||||
allowCustomValue?: boolean;
|
||||
isLoading?: boolean;
|
||||
newFormStylingEnabled?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -29,11 +24,9 @@ export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||
allowCustomValue = false,
|
||||
isLoading = false,
|
||||
inputId = label,
|
||||
newFormStylingEnabled,
|
||||
error,
|
||||
}: VariableQueryFieldProps<T>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return newFormStylingEnabled ? (
|
||||
return (
|
||||
<>
|
||||
<EditorField label={label} htmlFor={inputId} className={removeMarginBottom}>
|
||||
<Select
|
||||
@ -48,26 +41,5 @@ export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||
</EditorField>
|
||||
{error && <Alert title={error} severity="error" topSpacing={1} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={inputId}>
|
||||
<Select
|
||||
aria-label={label}
|
||||
width={25}
|
||||
allowCustomValue={allowCustomValue}
|
||||
value={value}
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
inputId={inputId}
|
||||
/>
|
||||
</InlineField>
|
||||
{error && <Alert className={styles.inlineFieldAlert} title={error} severity="error" topSpacing={1} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// width set to InlineField labelWidth + Select width + 0.5 for margin on the label
|
||||
inlineFieldAlert: css({ maxWidth: theme.spacing(LABEL_WIDTH + 25 + 0.5) }),
|
||||
});
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EditorField } from '@grafana/experimental';
|
||||
import { InlineField, Input, PopoverContent } from '@grafana/ui';
|
||||
import { Input, PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { removeMarginBottom } from '../styles';
|
||||
|
||||
const LABEL_WIDTH = 20;
|
||||
|
||||
interface Props {
|
||||
onBlur: (value: string) => void;
|
||||
value: string;
|
||||
@ -17,17 +15,9 @@ interface Props {
|
||||
newFormStylingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const VariableTextField = ({
|
||||
interactive,
|
||||
label,
|
||||
onBlur,
|
||||
placeholder,
|
||||
value,
|
||||
tooltip,
|
||||
newFormStylingEnabled,
|
||||
}: Props) => {
|
||||
export const VariableTextField = ({ interactive, label, onBlur, placeholder, value, tooltip }: Props) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
return newFormStylingEnabled ? (
|
||||
return (
|
||||
<EditorField label={label} tooltip={tooltip} tooltipInteractive={interactive} className={removeMarginBottom}>
|
||||
<Input
|
||||
aria-label={label}
|
||||
@ -37,16 +27,5 @@ export const VariableTextField = ({
|
||||
onBlur={() => onBlur(localValue)}
|
||||
/>
|
||||
</EditorField>
|
||||
) : (
|
||||
<InlineField interactive={interactive} label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip} grow>
|
||||
<Input
|
||||
aria-label={label}
|
||||
placeholder={placeholder}
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.currentTarget.value)}
|
||||
onBlur={() => onBlur(localValue)}
|
||||
width={25}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user