From 3605d85c4ceab46f3390444e8727b20bceb0ce8e Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:20:07 +0000 Subject: [PATCH] Dashboards: Remove `advancedDataSourcePicker` feature toggle (#81790) * remove advancedDataSourcePicker feature toggle from DataSourcePickerWithPrompt * remove advancedDataSourcePicker toggle from DataSourcePicker and adjust tests that relied on old picker * adjust failing tests in QueryVariableEditorForm * move DataSourceDropdown to DataSourcePicker file * covert style declaration syntax to object style in DataSourcePicker * remove advancedDataSourcePicker feature flag from registry * remove .only from test * adjust QueryVariableEditor test to avoid console.error --- .betterer.results | 18 +- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - .../src/selectors/components.ts | 5 + pkg/services/featuremgmt/registry.go | 11 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - .../RuleEditorCloudOnlyAllowed.test.tsx | 5 +- .../unified/RuleEditorCloudRules.test.tsx | 5 +- .../unified/RuleEditorRecordingRule.test.tsx | 7 +- .../CloudDataSourceSelector.tsx | 1 - public/app/features/alerting/unified/mocks.ts | 5 +- .../correlations/CorrelationsPage.test.tsx | 15 +- .../components/QueryVariableForm.test.tsx | 13 +- .../editors/QueryVariableEditor.test.tsx | 16 +- .../AnnotationsSettings.test.tsx | 4 +- .../picker/BuiltInDataSourceList.tsx | 3 +- .../components/picker/DataSourceDropdown.tsx | 437 ----------------- .../components/picker/DataSourceList.tsx | 7 +- ...own.test.tsx => DataSourcePicker.test.tsx} | 24 +- .../components/picker/DataSourcePicker.tsx | 447 +++++++++++++++++- .../explore/spec/helper/interactions.ts | 8 +- .../components/QueryEditorRowHeader.test.tsx | 3 +- .../features/query/components/QueryGroup.tsx | 2 +- .../adhoc/AdHocVariableEditor.test.tsx | 5 +- public/test/helpers/alertingRuleEditor.tsx | 2 +- 26 files changed, 505 insertions(+), 545 deletions(-) delete mode 100644 public/app/features/datasources/components/picker/DataSourceDropdown.tsx rename public/app/features/datasources/components/picker/{DataSourceDropdown.test.tsx => DataSourcePicker.test.tsx} (92%) diff --git a/.betterer.results b/.betterer.results index e24f6e23354..742646bc1ba 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2423,11 +2423,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Do not use any type assertions.", "8"] ], "public/app/features/alerting/unified/state/actions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -3146,16 +3144,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "7"], [0, 0, 0, "Styles should be written using objects.", "8"] ], - "public/app/features/datasources/components/picker/DataSourceDropdown.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] - ], "public/app/features/datasources/components/picker/DataSourceList.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index e9c5199ded5..2079e4b704e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -40,7 +40,6 @@ Some features are enabled by default. You can disable these feature by setting t | `lokiMetricDataplane` | Changes metric responses from Loki to be compliant with the dataplane specification. | Yes | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | Yes | -| `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order and advanced mode | Yes | | `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `transformationsRedesign` | Enables the transformations redesign | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 9a833fc0e81..e200c425377 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -78,7 +78,6 @@ export interface FeatureToggles { externalServiceAuth?: boolean; refactorVariablesTimeRange?: boolean; enableElasticsearchBackendQuerying?: boolean; - advancedDataSourcePicker?: boolean; faroDatasourceSelector?: boolean; enableDatagridEditing?: boolean; extraThemes?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 6a3f126b69d..82c905e8c4e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -404,6 +404,11 @@ export const Components = { */ input: () => 'input[id="data-source-picker"]', inputV2: 'data-testid Select a data source', + dataSourceList: 'data-testid Data source list dropdown', + advancedModal: { + dataSourceList: 'data-testid Data source list', + builtInDataSourceList: 'data-testid Built in data source list', + }, }, TimeZonePicker: { /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e9e85ae9f58..8ffaed7b6ac 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -509,17 +509,6 @@ var ( AllowSelfServe: true, Created: time.Date(2023, time.April, 14, 12, 0, 0, 0, time.UTC), }, - { - Name: "advancedDataSourcePicker", - Description: "Enable a new data source picker with contextual information, recently used order and advanced mode", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaDashboardsSquad, - AllowSelfServe: false, - HideFromAdminPage: true, - Created: time.Date(2023, time.April, 14, 12, 0, 0, 0, time.UTC), - }, { Name: "faroDatasourceSelector", Description: "Enable the data source selector within the Frontend Apps section of the Frontend Observability", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0f12326443a..7b4ff2e734c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -59,7 +59,6 @@ renderAuthJWT,preview,@grafana/grafana-as-code,2023-04-03,false,false,false externalServiceAuth,experimental,@grafana/identity-access-team,2023-04-11,true,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,2023-06-06,false,false,false enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,2023-04-14,false,false,false -advancedDataSourcePicker,GA,@grafana/dashboards-squad,2023-04-14,false,false,true faroDatasourceSelector,preview,@grafana/app-o11y,2023-05-04,false,false,true enableDatagridEditing,preview,@grafana/grafana-bi-squad,2023-04-24,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,2023-05-10,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7cb886c35d0..f8cfe3a4b92 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -247,10 +247,6 @@ const ( // Enable the processing of queries and responses in the Elasticsearch data source through backend FlagEnableElasticsearchBackendQuerying = "enableElasticsearchBackendQuerying" - // FlagAdvancedDataSourcePicker - // Enable a new data source picker with contextual information, recently used order and advanced mode - FlagAdvancedDataSourcePicker = "advancedDataSourcePicker" - // FlagFaroDatasourceSelector // Enable the data source selector within the Frontend Apps section of the Frontend Observability FlagFaroDatasourceSelector = "faroDatasourceSelector" diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx index 8c335a5a007..ea2a169d998 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx @@ -2,7 +2,7 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; -import { byRole, byText } from 'testing-library-selector'; +import { byText } from 'testing-library-selector'; import { setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -104,6 +104,7 @@ jest.mock('@grafana/runtime', () => ({ getDataSourceSrv: jest.fn(() => ({ getInstanceSettings: () => dataSources.prom, get: () => dataSources.prom, + getList: () => Object.values(dataSources), })), })); @@ -197,7 +198,7 @@ describe('RuleEditor cloud: checking editable data sources', () => { // check that only rules sources that have ruler available are there const dataSourceSelect = ui.inputs.dataSource.get(); - await userEvent.click(byRole('combobox').get(dataSourceSelect)); + await userEvent.click(dataSourceSelect); expect(byText('cortex with ruler').query()).toBeInTheDocument(); expect(byText('loki with ruler').query()).toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx index 4117406c67d..061b5dcbdb4 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { selectors } from '@grafana/e2e-selectors'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types'; @@ -138,11 +139,11 @@ describe('RuleEditor cloud', () => { //expressions are removed after switching to data-source managed expect(screen.queryAllByLabelText('Remove expression')).toHaveLength(0); - expect(screen.getByTestId('datasource-picker')).toBeInTheDocument(); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toBeInTheDocument(); const dataSourceSelect = await ui.inputs.dataSource.find(); await user.click(dataSourceSelect); - await clickSelectOption(dataSourceSelect, 'Prom (default)'); + await user.click(screen.getByText('Prom')); await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled()); await user.type(await ui.inputs.expr.find(), 'up == 1'); diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx index 681a84611b1..8b9455201f4 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx @@ -3,7 +3,7 @@ import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event' import React from 'react'; import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; -import { byRole, byText } from 'testing-library-selector'; +import { byText } from 'testing-library-selector'; import { setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -72,6 +72,7 @@ jest.mock('@grafana/runtime', () => ({ getDataSourceSrv: jest.fn(() => ({ getInstanceSettings: () => dataSources.default, get: () => dataSources.default, + getList: () => Object.values(dataSources), })), })); @@ -149,9 +150,9 @@ describe('RuleEditor recording rules', () => { await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule'); const dataSourceSelect = ui.inputs.dataSource.get(); - await userEvent.click(byRole('combobox').get(dataSourceSelect)); + await userEvent.click(dataSourceSelect); - await clickSelectOption(dataSourceSelect, 'Prom (default)'); + await userEvent.click(screen.getByText('Prom')); await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); await clickSelectOption(ui.inputs.group.get(), 'group2'); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx index 938550ae0d8..88e4ee494ce 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx @@ -32,7 +32,6 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C label={disabled ? 'Data source' : 'Select data source'} error={errors.dataSourceName?.message} invalid={!!errors.dataSourceName?.message} - data-testid="datasource-picker" > ( diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 88a216b58fb..3683e9f56ba 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -395,10 +395,7 @@ export class MockDataSourceSrv implements DataSourceSrv { * Get settings and plugin metadata by name or uid */ getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined { - return ( - DatasourceSrv.prototype.getInstanceSettings.call(this, nameOrUid) || - ({ meta: { info: { logos: {} } } } as unknown as DataSourceInstanceSettings) - ); + return DatasourceSrv.prototype.getInstanceSettings.call(this, nameOrUid); } async loadDatasource(name: string): Promise> { diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx index 108d6502849..f127acf5b0a 100644 --- a/public/app/features/correlations/CorrelationsPage.test.tsx +++ b/public/app/features/correlations/CorrelationsPage.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, screen, fireEvent, within, Matcher, getByRole } from '@testing-library/react'; +import { render, waitFor, screen, within, Matcher, getByRole } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { merge, uniqueId } from 'lodash'; import React from 'react'; @@ -9,6 +9,7 @@ import { MockDataSourceApi } from 'test/mocks/datasource_srv'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { DataSourcePluginMeta, SupportedTransformationType } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { BackendSrv, setDataSourceSrv, BackendSrvRequest, reportInteraction } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; @@ -270,13 +271,13 @@ describe('CorrelationsPage', () => { // step 2: // set target datasource picker value - fireEvent.keyDown(screen.getByLabelText(/^target/i), { keyCode: 40 }); + await userEvent.click(screen.getByLabelText(/^target/i)); await userEvent.click(screen.getByText('prometheus')); await userEvent.click(await screen.findByRole('button', { name: /next$/i })); // step 3: // set source datasource picker value - fireEvent.keyDown(screen.getByLabelText(/^source/i), { keyCode: 40 }); + await userEvent.click(screen.getByLabelText(/^source/i)); await userEvent.click(screen.getByText('loki')); await userEvent.click(await screen.findByRole('button', { name: /add$/i })); @@ -427,14 +428,16 @@ describe('CorrelationsPage', () => { // step 2: // set target datasource picker value - fireEvent.keyDown(screen.getByLabelText(/^target/i), { keyCode: 40 }); + await userEvent.click(screen.getByLabelText(/^target/i)); await userEvent.click(screen.getByText('elastic')); await userEvent.click(await screen.findByRole('button', { name: /next$/i })); // step 3: // set source datasource picker value - fireEvent.keyDown(screen.getByLabelText(/^source/i), { keyCode: 40 }); - await userEvent.click(within(screen.getByLabelText('Select options menu')).getByText('prometheus')); + await userEvent.click(screen.getByLabelText(/^source/i)); + await userEvent.click( + within(screen.getByTestId(selectors.components.DataSourcePicker.dataSourceList)).getByText('prometheus') + ); await userEvent.clear(screen.getByRole('textbox', { name: /results field/i })); await userEvent.type(screen.getByRole('textbox', { name: /results field/i }), 'Line'); diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 8e37364ccc7..9590377e41a 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -108,7 +108,7 @@ describe('QueryVariableEditorForm', () => { const { renderer: { getByTestId, getByRole }, } = setup(); - const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.container); + const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2); //const queryEditor = getByTestId('query-editor'); const regexInput = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 @@ -131,7 +131,7 @@ describe('QueryVariableEditorForm', () => { ); expect(dataSourcePicker).toBeInTheDocument(); - expect(dataSourcePicker).toHaveTextContent('Default Test Data Source'); + expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(regexInput).toBeInTheDocument(); expect(regexInput).toHaveValue('.*'); expect(sortSelect).toBeInTheDocument(); @@ -151,12 +151,11 @@ describe('QueryVariableEditorForm', () => { renderer: { getByTestId }, } = setup(); const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2); - await waitFor(async () => { - await userEvent.click(dataSourcePicker); // open the select - await userEvent.tab(); - }); + await userEvent.click(dataSourcePicker); + await userEvent.click(screen.getByText(/prometheus/i)); + expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1); - expect(mockOnDataSourceChange).toHaveBeenCalledWith(defaultDatasource); + expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); }); it('should call onQueryChange when changing the query', async () => { diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 867ecb3e836..efb730fe90b 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -93,7 +93,7 @@ describe('QueryVariableEditor', () => { it('should render the component with initializing the components correctly', async () => { const { renderer } = await setup(); - const dataSourcePicker = renderer.getByTestId(selectors.components.DataSourcePicker.container); + const dataSourcePicker = renderer.getByTestId(selectors.components.DataSourcePicker.inputV2); const queryEditor = renderer.getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput ); @@ -118,7 +118,7 @@ describe('QueryVariableEditor', () => { ); expect(dataSourcePicker).toBeInTheDocument(); - expect(dataSourcePicker).toHaveTextContent('Default Test Data Source'); + expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(queryEditor).toBeInTheDocument(); expect(queryEditor).toHaveValue('my-query'); expect(regexInput).toBeInTheDocument(); @@ -138,18 +138,20 @@ describe('QueryVariableEditor', () => { it('should update variable state when changing the datasource', async () => { const { variable, - renderer: { getByTestId }, + renderer: { getByTestId, getByText }, user, } = await setup(); - const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.container).getElementsByTagName('input'); + + expect(variable.state.datasource).toEqual({ uid: 'mock-ds-2', type: 'test' }); + + await user.click(getByTestId(selectors.components.DataSourcePicker.inputV2)); + await user.click(getByText(/prom/i)); await waitFor(async () => { - await user.type(dataSourcePicker[0], 'm'); - await user.tab(); await lastValueFrom(variable.validateAndUpdate()); }); - expect(variable.state.datasource).toEqual({ uid: 'mock-ds-2', type: 'test' }); + expect(variable.state.datasource).toEqual({ uid: 'mock-ds-3', type: 'prometheus' }); }); it('should update the variable state when changing the query', async () => { diff --git a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.test.tsx index 733f48f9c05..a5f0962c799 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.test.tsx @@ -205,10 +205,10 @@ describe('AnnotationsSettings', () => { await userEvent.clear(nameInput); await userEvent.type(nameInput, 'My Prometheus Annotation'); - await userEvent.click(screen.getByText(/testdata/i)); + await userEvent.click(screen.getByPlaceholderText(/testdata/i)); expect(await screen.findByText(/Prometheus/i)).toBeVisible(); - expect(screen.queryAllByText(/testdata/i)).toHaveLength(2); + expect(screen.queryAllByText(/testdata/i)).toHaveLength(1); await userEvent.click(screen.getByText(/prometheus/i)); diff --git a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx index ee08899e4a2..44c90bf819c 100644 --- a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx +++ b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { DataSourceInstanceSettings } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { DataSourceRef } from '@grafana/schema'; import { t } from 'app/core/internationalization'; @@ -69,7 +70,7 @@ export function BuiltInDataSourceList({ const filteredResults = grafanaDataSources.filter((ds) => (filter ? filter?.(ds) : true) && !!ds.meta.builtIn); return ( -
+
{filteredResults.map((ds) => { return ( void; - current?: DataSourceInstanceSettings | string | DataSourceRef | null; - recentlyUsed?: string[]; - hideTextValue?: boolean; - width?: number; - inputId?: string; - noDefault?: boolean; - disabled?: boolean; - placeholder?: string; - - // DS filters - tracing?: boolean; - mixed?: boolean; - dashboard?: boolean; - metrics?: boolean; - type?: string | string[]; - annotations?: boolean; - variables?: boolean; - alerting?: boolean; - pluginId?: string; - logs?: boolean; - uploadFile?: boolean; - filter?: (ds: DataSourceInstanceSettings) => boolean; -} - -export function DataSourceDropdown(props: DataSourceDropdownProps) { - const { - current, - onChange, - hideTextValue = false, - width, - inputId, - noDefault = false, - disabled = false, - placeholder = 'Select data source', - ...restProps - } = props; - - const styles = useStyles2(getStylesDropdown, props); - const [isOpen, setOpen] = useState(false); - const [inputHasFocus, setInputHasFocus] = useState(false); - const [filterTerm, setFilterTerm] = useState(''); - const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); - const ref = useRef(null); - - // Used to position the popper correctly and to bring back the focus when navigating from footer to input - const [markerElement, setMarkerElement] = useState(); - // Used to position the popper correctly - const [selectorElement, setSelectorElement] = useState(); - // Used to move the focus to the footer when tabbing from the input - const [footerRef, setFooterRef] = useState(); - const currentDataSourceInstanceSettings = useDatasource(current); - const grafanaDS = useDatasource('-- Grafana --'); - const currentValue = Boolean(!current && noDefault) ? undefined : currentDataSourceInstanceSettings; - const prefixIcon = - filterTerm && isOpen ? : ; - - const popper = usePopper(markerElement, selectorElement, { - placement: 'bottom-start', - modifiers: [ - { - name: 'offset', - options: { - offset: [0, 4], - }, - }, - maxSize, - applyMaxSize, - ], - }); - - const onClose = useCallback(() => { - setFilterTerm(''); - setOpen(false); - markerElement?.focus(); - }, [setOpen, markerElement]); - - const { overlayProps, underlayProps } = useOverlay( - { - onClose: onClose, - isDismissable: true, - isOpen, - shouldCloseOnInteractOutside: (element) => { - return markerElement ? !markerElement.isSameNode(element) : false; - }, - }, - ref - ); - const { dialogProps } = useDialog( - { - 'aria-label': 'Opened data source picker list', - }, - ref - ); - - function openDropdown() { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); - setOpen(true); - markerElement?.focus(); - } - - function onClickAddCSV() { - if (!grafanaDS) { - return; - } - - onChange(grafanaDS, [defaultFileUploadQuery]); - } - - function onKeyDownInput(keyEvent: React.KeyboardEvent) { - // From the input, it navigates to the footer - if (keyEvent.key === 'Tab' && !keyEvent.shiftKey && isOpen) { - keyEvent.preventDefault(); - footerRef?.focus(); - } - // From the input, if we navigate back, it closes the dropdown - if (keyEvent.key === 'Tab' && keyEvent.shiftKey && isOpen) { - onClose(); - } - onKeyDown(keyEvent); - } - - function onNavigateOutsiteFooter(e: React.KeyboardEvent) { - // When navigating back, the dropdown keeps open and the input element is focused. - if (e.shiftKey) { - e.preventDefault(); - markerElement?.focus(); - // When navigating forward, the dropdown closes and and the element next to the input element is focused. - } else { - onClose(); - } - } - - useEffect(() => { - const sub = keyboardEvents.subscribe({ - next: (keyEvent) => { - switch (keyEvent?.code) { - case 'ArrowDown': - openDropdown(); - keyEvent.preventDefault(); - break; - case 'ArrowUp': - openDropdown(); - keyEvent.preventDefault(); - break; - case 'Escape': - onClose(); - keyEvent.preventDefault(); - break; - } - }, - }); - return () => sub.unsubscribe(); - }); - - return ( -
- {/* This clickable div is just extending the clickable area on the input element to include the prefix and suffix. */} - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} -
- } - placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder} - onFocus={() => { - setInputHasFocus(true); - }} - onBlur={() => { - setInputHasFocus(false); - }} - onKeyDown={onKeyDownInput} - value={filterTerm} - onChange={(e) => { - openDropdown(); - setFilterTerm(e.currentTarget.value); - }} - ref={setMarkerElement} - disabled={disabled} - > -
- {isOpen ? ( - -
-
- { - onClose(); - if (ds.uid !== currentValue?.uid) { - onChange(ds, defaultQueries); - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SELECT_DS, ds_type: ds.type }); - } - }} - onClose={onClose} - onClickAddCSV={onClickAddCSV} - onDismiss={onClose} - onNavigateOutsiteFooter={onNavigateOutsiteFooter} - /> -
- - ) : null} -
- ); -} - -function getStylesDropdown(theme: GrafanaTheme2, props: DataSourceDropdownProps) { - return { - container: css` - position: relative; - cursor: ${props.disabled ? 'not-allowed' : 'pointer'}; - width: ${theme.spacing(props.width || 'auto')}; - `, - trigger: css` - cursor: pointer; - ${props.disabled && `pointer-events: none;`} - `, - input: css` - input::placeholder { - color: ${props.disabled ? theme.colors.action.disabledText : theme.colors.text.primary}; - } - `, - }; -} - -export interface PickerContentProps extends DataSourceDropdownProps { - onClickAddCSV?: () => void; - keyboardEvents: Observable; - style: React.CSSProperties; - filterTerm?: string; - onClose: () => void; - onDismiss: () => void; - footerRef: (element: HTMLElement | null) => void; - onNavigateOutsiteFooter: (e: React.KeyboardEvent) => void; -} - -const PickerContent = React.forwardRef((props, ref) => { - const { filterTerm, onChange, onClose, onClickAddCSV, current, filter } = props; - - const changeCallback = useCallback( - (ds: DataSourceInstanceSettings) => { - onChange(ds); - }, - [onChange] - ); - - const clickAddCSVCallback = useCallback(() => { - onClickAddCSV?.(); - onClose(); - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.ADD_FILE }); - }, [onClickAddCSV, onClose]); - - const styles = useStyles2(getStylesPickerContent); - - return ( -
- - (filter ? filter?.(ds) : true) && matchDataSourceWithSearch(ds, filterTerm)} - onClickEmptyStateCTA={() => - reportInteraction(INTERACTION_EVENT_NAME, { - item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE, - }) - } - > - - -
- -
- ); -}); -PickerContent.displayName = 'PickerContent'; - -function getStylesPickerContent(theme: GrafanaTheme2) { - return { - container: css` - display: flex; - flex-direction: column; - background: ${theme.colors.background.primary}; - box-shadow: ${theme.shadows.z3}; - `, - picker: css` - background: ${theme.colors.background.secondary}; - `, - dataSourceList: css` - flex: 1; - `, - footer: css` - flex: 0; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - padding: ${theme.spacing(1.5)}; - border-top: 1px solid ${theme.colors.border.weak}; - background-color: ${theme.colors.background.secondary}; - `, - }; -} - -export interface FooterProps extends PickerContentProps {} - -function Footer({ onClose, onChange, onClickAddCSV, ...props }: FooterProps) { - const styles = useStyles2(getStylesFooter); - const isUploadFileEnabled = props.uploadFile && config.featureToggles.editPanelCSVDragAndDrop; - - const onKeyDownLastButton = (e: React.KeyboardEvent) => { - if (e.key === 'Tab') { - props.onNavigateOutsiteFooter(e); - } - }; - const onKeyDownFirstButton = (e: React.KeyboardEvent) => { - if (e.key === 'Tab' && e.shiftKey) { - props.onNavigateOutsiteFooter(e); - } - }; - - return ( -
- - {({ showModal, hideModal }) => ( - - )} - - {isUploadFileEnabled && ( - - )} -
- ); -} - -function getStylesFooter(theme: GrafanaTheme2) { - return { - footer: css` - flex: 0; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - padding: ${theme.spacing(1.5)}; - border-top: 1px solid ${theme.colors.border.weak}; - background-color: ${theme.colors.background.secondary}; - `, - }; -} diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx index df286934acb..0d2238a8820 100644 --- a/public/app/features/datasources/components/picker/DataSourceList.tsx +++ b/public/app/features/datasources/components/picker/DataSourceList.tsx @@ -3,6 +3,7 @@ import React, { useRef } from 'react'; import { Observable } from 'rxjs'; import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { getTemplateSrv } from '@grafana/runtime'; import { useStyles2, useTheme2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; @@ -72,7 +73,11 @@ export function DataSourceList(props: DataSourceListProps) { const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources; return ( -
+
{filteredDataSources.length === 0 && ( )} diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.test.tsx similarity index 92% rename from public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx rename to public/app/features/datasources/components/picker/DataSourcePicker.test.tsx index 5b9ba51f778..c13140a43c3 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx +++ b/public/app/features/datasources/components/picker/DataSourcePicker.test.tsx @@ -9,7 +9,7 @@ import { ModalRoot, ModalsProvider } from '@grafana/ui'; import config from 'app/core/config'; import { defaultFileUploadQuery } from 'app/plugins/datasource/grafana/types'; -import { DataSourceDropdown, DataSourceDropdownProps } from './DataSourceDropdown'; +import { DataSourcePicker, DataSourcePickerProps } from './DataSourcePicker'; import * as utils from './utils'; const pluginMetaInfo: PluginMetaInfo = { @@ -45,8 +45,8 @@ const MockDSBuiltIn = createDS('mock.datasource.builtin', 3, true); const mockDSList = [mockDS1, mockDS2, MockDSBuiltIn]; -async function setupOpenDropdown(user: UserEvent, props: DataSourceDropdownProps) { - const dropdown = render(); +async function setupOpenDropdown(user: UserEvent, props: DataSourcePickerProps) { + const dropdown = render(); const searchBox = dropdown.container.querySelector('input'); expect(searchBox).toBeInTheDocument(); await user.click(searchBox!); @@ -93,9 +93,9 @@ beforeEach(() => { getInstanceSettingsMock.mockReturnValue(mockDS1); }); -describe('DataSourceDropdown', () => { +describe('DataSourcePicker', () => { it('should render', () => { - expect(() => render()).not.toThrow(); + expect(() => render()).not.toThrow(); }); describe('configuration', () => { @@ -123,7 +123,7 @@ describe('DataSourceDropdown', () => { render( - + ); @@ -145,7 +145,7 @@ describe('DataSourceDropdown', () => { it('should display the current selected DS in the selector', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); - render(); + render(); expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( 'placeholder', mockDS2.name @@ -169,7 +169,7 @@ describe('DataSourceDropdown', () => { it('should display the default DS as selected when `current` is not set', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); - render(); + render(); expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( 'placeholder', mockDS2.name @@ -186,12 +186,12 @@ describe('DataSourceDropdown', () => { }); it('should disable the dropdown when `disabled` is true', () => { - render(); + render(); expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toBeDisabled(); }); it('should assign the correct `id` to the input element to pair it with a label', () => { - render(); + render(); expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( 'id', 'custom.input.id' @@ -199,7 +199,7 @@ describe('DataSourceDropdown', () => { }); it('should not set the default DS when setting `noDefault` to true and `current` is not provided', () => { - render(); + render(); getListMock.mockClear(); getInstanceSettingsMock.mockClear(); // Doesn't try to get the default DS @@ -300,7 +300,7 @@ describe('DataSourceDropdown', () => { const props = { onChange: jest.fn(), current: mockDS1.name }; render( - + ); diff --git a/public/app/features/datasources/components/picker/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.tsx index 4c75756d497..efc6de67408 100644 --- a/public/app/features/datasources/components/picker/DataSourcePicker.tsx +++ b/public/app/features/datasources/components/picker/DataSourcePicker.tsx @@ -1,24 +1,437 @@ -import React from 'react'; +import { css } from '@emotion/css'; +import { useDialog } from '@react-aria/dialog'; +import { FocusScope } from '@react-aria/focus'; +import { useOverlay } from '@react-aria/overlays'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { usePopper } from 'react-popper'; +import { Observable } from 'rxjs'; -import { - DataSourcePicker as DeprecatedDataSourcePicker, - DataSourcePickerProps as DeprecatedDataSourcePickerProps, -} from '@grafana/runtime'; -import { config } from 'app/core/config'; +import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { reportInteraction } from '@grafana/runtime'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; +import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui'; +import config from 'app/core/config'; +import { Trans } from 'app/core/internationalization'; +import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection'; +import { defaultFileUploadQuery, GrafanaQuery } from 'app/plugins/datasource/grafana/types'; -import { DataSourceDropdown, DataSourceDropdownProps } from './DataSourceDropdown'; +import { useDatasource } from '../../hooks'; -type DataSourcePickerProps = DeprecatedDataSourcePickerProps | DataSourceDropdownProps; +import { DataSourceList } from './DataSourceList'; +import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo'; +import { DataSourceModal } from './DataSourceModal'; +import { applyMaxSize, maxSize } from './popperModifiers'; +import { dataSourceLabel, matchDataSourceWithSearch } from './utils'; + +const INTERACTION_EVENT_NAME = 'dashboards_dspicker_clicked'; +const INTERACTION_ITEM = { + OPEN_DROPDOWN: 'open_dspicker', + SELECT_DS: 'select_ds', + ADD_FILE: 'add_file', + OPEN_ADVANCED_DS_PICKER: 'open_advanced_ds_picker', + CONFIG_NEW_DS_EMPTY_STATE: 'config_new_ds_empty_state', +}; + +export interface DataSourcePickerProps { + onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void; + current?: DataSourceInstanceSettings | string | DataSourceRef | null; + recentlyUsed?: string[]; + hideTextValue?: boolean; + width?: number; + inputId?: string; + noDefault?: boolean; + disabled?: boolean; + placeholder?: string; + + // DS filters + tracing?: boolean; + mixed?: boolean; + dashboard?: boolean; + metrics?: boolean; + type?: string | string[]; + annotations?: boolean; + variables?: boolean; + alerting?: boolean; + pluginId?: string; + logs?: boolean; + uploadFile?: boolean; + filter?: (ds: DataSourceInstanceSettings) => boolean; +} -/** - * DataSourcePicker is a wrapper around the old DataSourcePicker and the new one. - * Depending on the feature toggle, it will render the old or the new one. - * Feature toggle: advancedDataSourcePicker - */ export function DataSourcePicker(props: DataSourcePickerProps) { - return !config.featureToggles.advancedDataSourcePicker ? ( - - ) : ( - + const { + current, + onChange, + hideTextValue = false, + width, + inputId, + noDefault = false, + disabled = false, + placeholder = 'Select data source', + ...restProps + } = props; + + const styles = useStyles2(getStylesDropdown, props); + const [isOpen, setOpen] = useState(false); + const [inputHasFocus, setInputHasFocus] = useState(false); + const [filterTerm, setFilterTerm] = useState(''); + const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); + const ref = useRef(null); + + // Used to position the popper correctly and to bring back the focus when navigating from footer to input + const [markerElement, setMarkerElement] = useState(); + // Used to position the popper correctly + const [selectorElement, setSelectorElement] = useState(); + // Used to move the focus to the footer when tabbing from the input + const [footerRef, setFooterRef] = useState(); + const currentDataSourceInstanceSettings = useDatasource(current); + const grafanaDS = useDatasource('-- Grafana --'); + const currentValue = Boolean(!current && noDefault) ? undefined : currentDataSourceInstanceSettings; + const prefixIcon = + filterTerm && isOpen ? : ; + + const popper = usePopper(markerElement, selectorElement, { + placement: 'bottom-start', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 4], + }, + }, + maxSize, + applyMaxSize, + ], + }); + + const onClose = useCallback(() => { + setFilterTerm(''); + setOpen(false); + markerElement?.focus(); + }, [setOpen, markerElement]); + + const { overlayProps, underlayProps } = useOverlay( + { + onClose: onClose, + isDismissable: true, + isOpen, + shouldCloseOnInteractOutside: (element) => { + return markerElement ? !markerElement.isSameNode(element) : false; + }, + }, + ref + ); + const { dialogProps } = useDialog( + { + 'aria-label': 'Opened data source picker list', + }, + ref ); + + function openDropdown() { + reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); + setOpen(true); + markerElement?.focus(); + } + + function onClickAddCSV() { + if (!grafanaDS) { + return; + } + + onChange(grafanaDS, [defaultFileUploadQuery]); + } + + function onKeyDownInput(keyEvent: React.KeyboardEvent) { + // From the input, it navigates to the footer + if (keyEvent.key === 'Tab' && !keyEvent.shiftKey && isOpen) { + keyEvent.preventDefault(); + footerRef?.focus(); + } + // From the input, if we navigate back, it closes the dropdown + if (keyEvent.key === 'Tab' && keyEvent.shiftKey && isOpen) { + onClose(); + } + onKeyDown(keyEvent); + } + + function onNavigateOutsiteFooter(e: React.KeyboardEvent) { + // When navigating back, the dropdown keeps open and the input element is focused. + if (e.shiftKey) { + e.preventDefault(); + markerElement?.focus(); + // When navigating forward, the dropdown closes and and the element next to the input element is focused. + } else { + onClose(); + } + } + + useEffect(() => { + const sub = keyboardEvents.subscribe({ + next: (keyEvent) => { + switch (keyEvent?.code) { + case 'ArrowDown': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'ArrowUp': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'Escape': + onClose(); + keyEvent.preventDefault(); + break; + } + }, + }); + return () => sub.unsubscribe(); + }); + + return ( +
+ {/* This clickable div is just extending the clickable area on the input element to include the prefix and suffix. */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ } + placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder} + onFocus={() => { + setInputHasFocus(true); + }} + onBlur={() => { + setInputHasFocus(false); + }} + onKeyDown={onKeyDownInput} + value={filterTerm} + onChange={(e) => { + openDropdown(); + setFilterTerm(e.currentTarget.value); + }} + ref={setMarkerElement} + disabled={disabled} + > +
+ {isOpen ? ( + +
+
+ { + onClose(); + if (ds.uid !== currentValue?.uid) { + onChange(ds, defaultQueries); + reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SELECT_DS, ds_type: ds.type }); + } + }} + onClose={onClose} + onClickAddCSV={onClickAddCSV} + onDismiss={onClose} + onNavigateOutsiteFooter={onNavigateOutsiteFooter} + /> +
+ + ) : null} +
+ ); +} + +function getStylesDropdown(theme: GrafanaTheme2, props: DataSourcePickerProps) { + return { + container: css({ + position: 'relative', + cursor: props.disabled ? 'not-allowed' : 'pointer', + width: theme.spacing(props.width || 'auto'), + }), + trigger: css({ + cursor: 'pointer', + pointerEvents: props.disabled ? 'none' : 'auto', + }), + input: css({ + 'input::placeholder': { + color: props.disabled ? theme.colors.action.disabledText : theme.colors.text.primary, + }, + }), + }; +} + +export interface PickerContentProps extends DataSourcePickerProps { + onClickAddCSV?: () => void; + keyboardEvents: Observable; + style: React.CSSProperties; + filterTerm?: string; + onClose: () => void; + onDismiss: () => void; + footerRef: (element: HTMLElement | null) => void; + onNavigateOutsiteFooter: (e: React.KeyboardEvent) => void; +} + +const PickerContent = React.forwardRef((props, ref) => { + const { filterTerm, onChange, onClose, onClickAddCSV, current, filter } = props; + + const changeCallback = useCallback( + (ds: DataSourceInstanceSettings) => { + onChange(ds); + }, + [onChange] + ); + + const clickAddCSVCallback = useCallback(() => { + onClickAddCSV?.(); + onClose(); + reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.ADD_FILE }); + }, [onClickAddCSV, onClose]); + + const styles = useStyles2(getStylesPickerContent); + + return ( +
+ + (filter ? filter?.(ds) : true) && matchDataSourceWithSearch(ds, filterTerm)} + onClickEmptyStateCTA={() => + reportInteraction(INTERACTION_EVENT_NAME, { + item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE, + }) + } + > + + +
+ +
+ ); +}); +PickerContent.displayName = 'PickerContent'; + +function getStylesPickerContent(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + flexDirection: 'column', + background: theme.colors.background.primary, + boxShadow: theme.shadows.z3, + }), + picker: css({ + background: theme.colors.background.secondary, + }), + dataSourceList: css({ + flex: 1, + }), + footer: css({ + flex: 0, + display: 'flex', + flexDirection: 'row-reverse', + justifyContent: 'space-between', + padding: theme.spacing(1.5), + borderTop: `1px solid ${theme.colors.border.weak}`, + backgroundColor: theme.colors.background.secondary, + }), + }; +} + +export interface FooterProps extends PickerContentProps {} + +function Footer({ onClose, onChange, onClickAddCSV, ...props }: FooterProps) { + const styles = useStyles2(getStylesFooter); + const isUploadFileEnabled = props.uploadFile && config.featureToggles.editPanelCSVDragAndDrop; + + const onKeyDownLastButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + props.onNavigateOutsiteFooter(e); + } + }; + const onKeyDownFirstButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab' && e.shiftKey) { + props.onNavigateOutsiteFooter(e); + } + }; + + return ( +
+ + {({ showModal, hideModal }) => ( + + )} + + {isUploadFileEnabled && ( + + )} +
+ ); +} + +function getStylesFooter(theme: GrafanaTheme2) { + return { + footer: css({ + flex: 0, + display: 'flex', + flexDirection: 'row-reverse', + justifyContent: 'space-between', + padding: theme.spacing(1.5), + borderTop: `1px solid ${theme.colors.border.weak}`, + backgroundColor: theme.colors.background.secondary, + }), + }; } diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 523a48db1ae..3afd11fef18 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -1,4 +1,4 @@ -import { fireEvent, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { selectors } from '@grafana/e2e-selectors'; @@ -7,9 +7,9 @@ import { getAllByRoleInQueryHistoryTab, withinExplore } from './setup'; export const changeDatasource = async (name: string) => { const datasourcePicker = (await screen.findByTestId(selectors.components.DataSourcePicker.container)).children[0]; - fireEvent.keyDown(datasourcePicker, { keyCode: 40 }); - const option = screen.getByText(name); - fireEvent.click(option); + await userEvent.click(datasourcePicker); + const option = within(screen.getByTestId(selectors.components.DataSourcePicker.dataSourceList)).getAllByText(name)[0]; + await userEvent.click(option); }; export const inputQuery = async (query: string, exploreId = 'left') => { diff --git a/public/app/features/query/components/QueryEditorRowHeader.test.tsx b/public/app/features/query/components/QueryEditorRowHeader.test.tsx index a3fdae682bf..c0bdd036d6f 100644 --- a/public/app/features/query/components/QueryEditorRowHeader.test.tsx +++ b/public/app/features/query/components/QueryEditorRowHeader.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { openMenu } from 'react-select-event'; import { DataSourceInstanceSettings } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -84,7 +83,7 @@ describe('QueryEditorRowHeader', () => { renderScenario({ onChangeDataSource: () => {} }); const dsSelect = screen.getByTestId(selectors.components.DataSourcePicker.container).querySelector('input')!; - openMenu(dsSelect); + userEvent.click(dsSelect); expect(await screen.findByText('${dsVariable}')).toBeInTheDocument(); }); }); diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index e9039cdbf2d..1ec9dbad345 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -497,7 +497,7 @@ function DataSourcePickerWithPrompt({ options, onChange, ...otherProps }: DataSo return ( <> - {isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && ( + {isDataSourceModalOpen && ( setIsDataSourceModalOpen(false)}> )} diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx index 3d5607e92ed..87e640539ac 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React, { ComponentProps } from 'react'; -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { selectors } from '@grafana/e2e-selectors'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; @@ -68,7 +68,8 @@ describe('AdHocVariableEditor', () => { const selectEl = screen .getByTestId(selectors.components.DataSourcePicker.container) .getElementsByTagName('input')[0]; - await selectOptionInTest(selectEl, 'Loki'); + await userEvent.click(selectEl); + await userEvent.click(screen.getByText('Loki')); expect(props.changeVariableDatasource).toBeCalledWith( { type: 'adhoc', id: 'adhoc', rootStateKey: 'key' }, diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index 7752de2b092..2ef5494a9db 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -13,7 +13,7 @@ export const ui = { inputs: { name: byRole('textbox', { name: 'name' }), alertType: byTestId('alert-type-picker'), - dataSource: byTestId('datasource-picker'), + dataSource: byTestId(selectors.components.DataSourcePicker.inputV2), folder: byTestId('folder-picker'), folderContainer: byTestId(selectors.components.FolderPicker.containerV2), namespace: byTestId('namespace-picker'),