diff --git a/.betterer.results b/.betterer.results index 31f7ab330d7..cc9ca97d137 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5413,9 +5413,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LegacyLogGroupNamesSelection.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsField.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/package.json b/package.json index a79e04ce655..fde4310d04e 100644 --- a/package.json +++ b/package.json @@ -242,7 +242,7 @@ "@fingerprintjs/fingerprintjs": "^3.4.2", "@glideapps/glide-data-grid": "^5.2.1", "@grafana-plugins/grafana-testdata-datasource": "workspace:*", - "@grafana/aws-sdk": "0.2.0", + "@grafana/aws-sdk": "0.3.1", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.7.4", diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx index ebcab303759..31dc0f98646 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx @@ -5,6 +5,7 @@ import { Provider } from 'react-redux'; import { AwsAuthType } from '@grafana/aws-sdk'; import { PluginContextProvider, PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { CloudWatchSettings, setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; @@ -128,178 +129,205 @@ describe('Render', () => { datasource.getVariables = jest.fn().mockReturnValue([]); }); - 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()); - }); + const originalFormFeatureToggleValue = config.featureToggles.awsDatasourcesNewFormStyling; - it('should show credentials profile name field', async () => { - setup({ - jsonData: { - authType: AwsAuthType.Credentials, - }, - }); - await waitFor(async () => expect(screen.getByLabelText('Credentials Profile Name')).toBeInTheDocument()); - }); + const cleanupFeatureToggle = () => { + config.featureToggles.awsDatasourcesNewFormStyling = originalFormFeatureToggleValue; + }; - 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 arn role field', async () => { - setup({ - jsonData: { - authType: AwsAuthType.ARN, - }, - }); - await waitFor(async () => expect(screen.getByLabelText('Assume Role ARN')).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( - - - - ); - await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); - const rerenderProps = { - ...newProps, - options: { - ...newProps.options, - jsonData: { - ...newProps.options.jsonData, - authType: AwsAuthType.Default, + function run() { + it('it should disable access key id field when the datasource has been previously configured', async () => { + setup({ + secureJsonFields: { + secretKey: true, }, - }, - }; - rerender( - - - - ); - 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() - ); - }); + }); + await waitFor(async () => expect(screen.getByPlaceholderText('Configured')).toBeDisabled()); + }); - 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( - - - - ); - await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); - const rerenderProps = { - ...newProps, - options: { - ...newProps.options, + 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 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 arn role field', async () => { + setup({ + jsonData: { + authType: AwsAuthType.ARN, + }, + }); + await waitFor(async () => expect(screen.getByText('Assume Role ARN')).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, - }, - }; - rerender( - - - - ); - await userEvent.click(screen.getByText('Select log groups')); - await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument()); + 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( + + + + ); + await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); + const rerenderProps = { + ...newProps, + options: { + ...newProps.options, + jsonData: { + ...newProps.options.jsonData, + authType: AwsAuthType.Default, + }, + }, + }; + rerender( + + + + ); + 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( + + + + ); + await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); + const rerenderProps = { + ...newProps, + options: { + ...newProps.options, + version: 2, + }, + }; + rerender( + + + + ); + 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(); + }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx index 9c5e38ced23..6c102817dc4 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx @@ -10,8 +10,9 @@ import { DataSourceTestSucceeded, DataSourceTestFailed, } from '@grafana/data'; +import { ConfigSection } from '@grafana/experimental'; import { getAppEvents, usePluginInteractionReporter } from '@grafana/runtime'; -import { Input, InlineField, FieldProps, SecureSocksProxySettings } from '@grafana/ui'; +import { Input, InlineField, FieldProps, SecureSocksProxySettings, Field, Divider } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { config } from 'app/core/config'; import { createWarningNotification } from 'app/core/copy/appNotification'; @@ -23,6 +24,7 @@ import { SelectableResourceValue } from '../../resources/types'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../../types'; import { LogGroupsFieldWrapper } from '../shared/LogGroups/LogGroupsField'; +import { SecureSocksProxySettingsNewStyling } from './SecureSocksProxySettingsNewStyling'; import { XrayLinkConfig } from './XrayLinkConfig'; export type Props = DataSourcePluginOptionsEditorProps; @@ -39,6 +41,7 @@ export const ConfigEditor = (props: Props) => { const [logGroupFieldState, setLogGroupFieldState] = useState({ invalid: false, }); + const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling; useEffect(() => setLogGroupFieldState({ invalid: false }), [props.options]); const report = usePluginInteractionReporter(); useEffect(() => { @@ -67,7 +70,103 @@ export const ConfigEditor = (props: Props) => { } }, [datasource, externalId]); - return ( + return newFormStylingEnabled ? ( +
+ { + return datasource.resources + .getRegions() + .then((regions) => + regions.reduce( + (acc: string[], curr: SelectableResourceValue) => (curr.value ? [...acc, curr.value] : acc), + [] + ) + ); + }) + } + externalId={externalId} + /> + {config.secureSocksDSProxyEnabled && ( + + )} + + + + + + + {datasource ? ( + { + 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); + }} + /> + ) : ( + <> + )} + + + + updateDatasourcePluginJsonDataOption(props, 'tracingDatasourceUid', uid)} + datasourceUid={options.jsonData.tracingDatasourceUid} + /> +
+ ) : ( <> + extends Pick, 'options' | 'onOptionsChange'> {} + +export interface SecureSocksProxyConfig extends DataSourceJsonData { + enableSecureSocksProxy?: boolean; +} + +export function SecureSocksProxySettingsNewStyling({ + options, + onOptionsChange, +}: Props): JSX.Element { + return ( + + + + onOptionsChange({ + ...options, + jsonData: { ...options.jsonData, enableSecureSocksProxy: event.currentTarget.checked }, + }) + } + /> + + + ); +} diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx index 107a11e42d3..619621253b6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx @@ -2,7 +2,8 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2, DataSourceInstanceSettings } from '@grafana/data'; -import { Alert, InlineField, useStyles2 } from '@grafana/ui'; +import { ConfigSection } from '@grafana/experimental'; +import { Alert, Field, InlineField, useStyles2 } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -16,16 +17,39 @@ const getStyles = (theme: GrafanaTheme2) => ({ interface Props { datasourceUid?: string; onChange: (uid: string) => void; + newFormStyling?: boolean; } const xRayDsId = 'grafana-x-ray-datasource'; -export function XrayLinkConfig({ datasourceUid, onChange }: Props) { +export function XrayLinkConfig({ newFormStyling, datasourceUid, onChange }: Props) { const hasXrayDatasource = Boolean(getDatasourceSrv().getList({ pluginId: xRayDsId }).length); const styles = useStyles2(getStyles); - return ( + return newFormStyling ? ( + + {!hasXrayDatasource && ( + + )} + + onChange(ds.uid)} + current={datasourceUid} + noDefault={true} + /> + + + ) : ( <>

X-ray trace link

diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx index b2a065e8050..a5f5414da48 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx @@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; 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'; @@ -23,6 +25,12 @@ 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' }, @@ -75,195 +83,216 @@ describe('VariableEditor', () => { beforeEach(() => { onChange.mockClear(); }); - 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(); - - 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(); - - 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 () => { + 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(); - }); - 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 - await select(keySelect, 'v4', { - container: document.body, - }); - expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ - namespace: 'z2', - region: 'a1', - metricName: 'i3', - dimensionFilters: undefined, - }); - 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' }, + 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(); + }); }); }); - 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(); - await waitFor(() => { - expect(screen.getByDisplayValue('Tags.blah')).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(); + + 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(); + }); + const filterItem = screen.getByTestId('cloudwatch-dimensions-filter-item'); + expect(filterItem).toBeInTheDocument(); + expect(within(filterItem).getByText('s4')).toBeInTheDocument(); + expect(within(filterItem).getByText('foo')).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(); - - 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', { + // change filter key + const keySelect = screen.getByRole('combobox', { name: 'Dimensions filter key' }); + // confirms getDimensionKeys was called with filter and that the element uses keysForDimensionFilter + await select(keySelect, 'v4', { container: document.body, - }) - ); + }); + expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ + namespace: 'z2', + region: 'a1', + metricName: 'i3', + dimensionFilters: undefined, + }); + expect(onChange).toHaveBeenCalledWith({ + ...defaultQuery, + queryType: VariableQueryType.DimensionValues, + namespace: 'z2', + region: 'a1', + metricName: 'i3', + dimensionKey: 's4', + dimensionFilters: { v4: undefined }, + }); - 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: {}, + // 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(); + + 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('LogGroups queryType is selected', () => { - it('should only render region and prefix', async () => { - const props = defaultProps; - props.query = { - ...defaultQuery, - queryType: VariableQueryType.LogGroups, - }; - render(); + 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(); - await waitFor(() => { - screen.getByLabelText('Log group prefix'); - screen.getByLabelText('Region'); + 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(); - expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument(); + await waitFor(() => { + screen.getByLabelText('Log group prefix'); + screen.getByLabelText('Region'); + }); + + expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument(); + }); + }); + } + + 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; + }); + afterAll(() => { + cleanupFeatureToggle(); + }); + run(); }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx index b337ef7ca1b..a0f6b550306 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { EditorField } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { InlineField } from '@grafana/ui'; @@ -44,6 +45,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { const keysForDimensionFilter = useDimensionKeys(datasource, { region, namespace, metricName, dimensionFilters }); const accountState = useAccountOptions(datasource.resources, query.region); + const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling; const onRegionChange = async (region: string) => { const validatedQuery = await sanitizeQuery({ ...parsedQuery, @@ -122,6 +124,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { } label="Query type" inputId={`variable-query-type-${query.refId}`} + newFormStylingEnabled={newFormStylingEnabled} /> {hasRegionField && ( { label="Region" isLoading={regionIsLoading} inputId={`variable-query-region-${query.refId}`} + newFormStylingEnabled={newFormStylingEnabled} /> )} {hasAccountIDField && @@ -143,6 +147,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { onChange={(accountId?: string) => onQueryChange({ ...parsedQuery, accountId })} options={[ALL_ACCOUNTS_OPTION, ...accountState?.value]} allowCustomValue={false} + newFormStylingEnabled={newFormStylingEnabled} /> )} {hasNamespaceField && ( @@ -153,6 +158,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { label="Namespace" inputId={`variable-query-namespace-${query.refId}`} allowCustomValue + newFormStylingEnabled={newFormStylingEnabled} /> )} {parsedQuery.queryType === VariableQueryType.DimensionValues && ( @@ -164,6 +170,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { label="Metric" inputId={`variable-query-metric-${query.refId}`} allowCustomValue + newFormStylingEnabled={newFormStylingEnabled} /> { label="Dimension key" inputId={`variable-query-dimension-key-${query.refId}`} allowCustomValue + newFormStylingEnabled={newFormStylingEnabled} /> - - { - onChange({ ...parsedQuery, dimensionFilters: dimensions }); - }} - dimensionKeys={keysForDimensionFilter} - disableExpressions={true} - datasource={datasource} - /> - + {newFormStylingEnabled ? ( + + { + onChange({ ...parsedQuery, dimensionFilters: dimensions }); + }} + dimensionKeys={keysForDimensionFilter} + disableExpressions={true} + datasource={datasource} + /> + + ) : ( + + { + onChange({ ...parsedQuery, dimensionFilters: dimensions }); + }} + dimensionKeys={keysForDimensionFilter} + disableExpressions={true} + datasource={datasource} + /> + + )} )} {parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && ( @@ -192,6 +219,7 @@ 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 && ( @@ -201,6 +229,7 @@ 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.". '} @@ -214,31 +243,58 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { } /> - - - Pre-defined ec2:DescribeInstances filters/tags - - {' and the values to filter on. Tags should be formatted tag:.'} - - } - > - { - onChange({ ...parsedQuery, ec2Filters: filters }); - }} - keyPlaceholder="filter/tag" - /> - + {newFormStylingEnabled ? ( + + + Pre-defined ec2:DescribeInstances filters/tags + + {' and the values to filter on. Tags should be formatted tag:.'} + + } + > + { + onChange({ ...parsedQuery, ec2Filters: filters }); + }} + keyPlaceholder="filter/tag" + /> + + ) : ( + + + Pre-defined ec2:DescribeInstances filters/tags + + {' and the values to filter on. Tags should be formatted tag:.'} + + } + > + { + onChange({ ...parsedQuery, ec2Filters: filters }); + }} + keyPlaceholder="filter/tag" + /> + + )} )} {parsedQuery.queryType === VariableQueryType.ResourceArns && ( @@ -247,16 +303,29 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { value={parsedQuery.resourceType} onBlur={(value: string) => onQueryChange({ ...parsedQuery, resourceType: value })} label="Resource type" + newFormStylingEnabled={newFormStylingEnabled} /> - - { - onChange({ ...parsedQuery, tags: filters }); - }} - keyPlaceholder="tag" - /> - + {newFormStylingEnabled ? ( + + { + onChange({ ...parsedQuery, tags: filters }); + }} + keyPlaceholder="tag" + /> + + ) : ( + + { + onChange({ ...parsedQuery, tags: filters }); + }} + keyPlaceholder="tag" + /> + + )} )} {parsedQuery.queryType === VariableQueryType.LogGroups && ( @@ -264,6 +333,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { value={query.logGroupPrefix ?? ''} onBlur={(value: string) => onQueryChange({ ...parsedQuery, logGroupPrefix: value })} label="Log group prefix" + newFormStylingEnabled={newFormStylingEnabled} /> )} diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx index 4c7598e5a30..ae4c94d2703 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; +import { EditorField } from '@grafana/experimental'; import { InlineField, Select } from '@grafana/ui'; import { VariableQueryType } from '../../types'; +import { removeMarginBottom } from '../styles'; const LABEL_WIDTH = 20; @@ -15,6 +17,7 @@ interface VariableQueryFieldProps { inputId?: string; allowCustomValue?: boolean; isLoading?: boolean; + newFormStylingEnabled?: boolean; } export const VariableQueryField = ({ @@ -25,8 +28,21 @@ export const VariableQueryField = ({ allowCustomValue = false, isLoading = false, inputId = label, + newFormStylingEnabled, }: VariableQueryFieldProps) => { - return ( + return newFormStylingEnabled ? ( + + { +export const VariableTextField = ({ + interactive, + label, + onBlur, + placeholder, + value, + tooltip, + newFormStylingEnabled, +}: Props) => { const [localValue, setLocalValue] = useState(value); - return ( + return newFormStylingEnabled ? ( + + setLocalValue(e.currentTarget.value)} + onBlur={() => onBlur(localValue)} + /> + + ) : ( void; }; -const rowGap = css` - gap: 3px; -`; +const rowGap = css({ + gap: 3, +}); +const logGroupNewStyles = css({ + display: 'flex', + flexDirection: 'column', + marginTop: 8, + '& div:first-child': { + marginBottom: 8, + }, +}); // used in Config Editor and in Log Query Editor export const LogGroupsField = ({ datasource, @@ -35,6 +44,7 @@ export const LogGroupsField = ({ logGroups, region, maxNoOfVisibleLogGroups, + newFormStylingEnabled, onBeforeOpen, }: Props) => { const accountState = useAccountOptions(datasource?.resources, region); @@ -74,7 +84,7 @@ export const LogGroupsField = ({ }, [datasource, legacyLogGroupNames, logGroups, onChange, region, loadingLogGroupsStarted]); return ( -
+
) => datasource?.resources.getLogGroups({ region: region, ...params }) ?? [] @@ -104,6 +114,7 @@ type WrapperProps = { region: string; maxNoOfVisibleLogGroups?: number; onBeforeOpen?: () => void; + newFormStylingEnabled?: boolean; // Legacy Props, can remove once we remove support for Legacy Log Group Selector legacyOnChange: (logGroups: string[]) => void; diff --git a/public/app/plugins/datasource/cloudwatch/components/styles.ts b/public/app/plugins/datasource/cloudwatch/components/styles.ts index 7c0025d2d39..783d8723313 100644 --- a/public/app/plugins/datasource/cloudwatch/components/styles.ts +++ b/public/app/plugins/datasource/cloudwatch/components/styles.ts @@ -96,5 +96,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ marginLeft: theme.spacing(0.5), }), }); +export const removeMarginBottom = css({ marginBottom: 8 }); export default getStyles; diff --git a/yarn.lock b/yarn.lock index d691a668ea5..d2539274ffb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2913,13 +2913,13 @@ __metadata: languageName: node linkType: hard -"@grafana/aws-sdk@npm:0.2.0": - version: 0.2.0 - resolution: "@grafana/aws-sdk@npm:0.2.0" +"@grafana/aws-sdk@npm:0.3.1": + version: 0.3.1 + resolution: "@grafana/aws-sdk@npm:0.3.1" dependencies: "@grafana/async-query-data": "npm:0.1.4" - "@grafana/experimental": "npm:1.1.0" - checksum: 5f79f62c37dc5b0841a38c0b9bf3d9d687e8e28c40a583399a47f7b7370dd07b0f5fd4d26ede496a453051ae385ea20e86edfdef4c55c2dceb43ab4400de634b + "@grafana/experimental": "npm:1.7.0" + checksum: 89b42fa6351b78ce9760fb07ebfad37d45aa3327858ad4e88acc1699ccc9d6541ba0232ba8331d6b0a6b61a1d6b134e19ae5e91b2e90353a97958291c15bc9ef languageName: node linkType: hard @@ -3096,24 +3096,6 @@ __metadata: languageName: unknown linkType: soft -"@grafana/experimental@npm:1.1.0": - version: 1.1.0 - resolution: "@grafana/experimental@npm:1.1.0" - dependencies: - "@types/uuid": "npm:^8.3.3" - uuid: "npm:^8.3.2" - peerDependencies: - "@emotion/css": 11.1.3 - "@grafana/data": ^9.2.0 - "@grafana/runtime": ^9.2.0 - "@grafana/ui": ^9.2.0 - react: 17.0.2 - react-dom: 17.0.2 - react-select: ^5.2.1 - checksum: 5e6b7ddf1a33d84a8bae11b241aaf6bc3a7c672457a91e92745189a17cb60cc170260e584d281ec3be72cb440766fefdcf85a73889d384b975c061872a8a910a - languageName: node - linkType: hard - "@grafana/experimental@npm:1.7.0": version: 1.7.0 resolution: "@grafana/experimental@npm:1.7.0" @@ -17311,7 +17293,7 @@ __metadata: "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@glideapps/glide-data-grid": "npm:^5.2.1" "@grafana-plugins/grafana-testdata-datasource": "workspace:*" - "@grafana/aws-sdk": "npm:0.2.0" + "@grafana/aws-sdk": "npm:0.3.1" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": "npm:6.0.1"