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:
Ida Štambuk 2024-07-22 14:48:17 +02:00 committed by GitHub
parent b7379b7b51
commit 5b17cd93c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 478 additions and 756 deletions

View File

@ -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"]
],

View File

@ -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 |

View File

@ -122,7 +122,6 @@ export interface FeatureToggles {
recoveryThreshold?: boolean;
lokiStructuredMetadata?: boolean;
teamHttpHeaders?: boolean;
awsDatasourcesNewFormStyling?: boolean;
cachingOptimizeSerializationMemoryUsage?: boolean;
panelTitleSearchInV1?: boolean;
managedPluginsInstall?: boolean;

View File

@ -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.",

View File

@ -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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
103 recoveryThreshold GA @grafana/alerting-squad false true false
104 lokiStructuredMetadata GA @grafana/observability-logs false false false
105 teamHttpHeaders preview @grafana/identity-access-team false false false
awsDatasourcesNewFormStyling GA @grafana/aws-datasources false false true
106 cachingOptimizeSerializationMemoryUsage experimental @grafana/grafana-operator-experience-squad false false false
107 panelTitleSearchInV1 experimental @grafana/search-and-storage true false false
108 managedPluginsInstall GA @grafana/plugins-platform-backend false false false

View File

@ -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"

View File

@ -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"
}

View File

@ -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());
});
});

View File

@ -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),
}),
});

View File

@ -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();
});
});
});

View File

@ -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),
}),
});

View File

@ -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) }),
});

View File

@ -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>
);
};