diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx
index 33cb862eb13..4ba217a3e2f 100644
--- a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx
@@ -8,7 +8,7 @@ import { ModalRoot, ModalsProvider } from '@grafana/ui';
import config from 'app/core/config';
import { defaultFileUploadQuery } from 'app/plugins/datasource/grafana/types';
-import { DataSourceDropdown } from './DataSourceDropdown';
+import { DataSourceDropdown, DataSourceDropdownProps } from './DataSourceDropdown';
import * as utils from './utils';
const pluginMetaInfo: PluginMetaInfo = {
@@ -44,14 +44,8 @@ const MockDSBuiltIn = createDS('mock.datasource.builtin', 3, true);
const mockDSList = [mockDS1, mockDS2, MockDSBuiltIn];
-const setup = (onChange = () => {}, current = mockDS1.name) => {
- const props = { onChange, current };
- window.HTMLElement.prototype.scrollIntoView = jest.fn();
- return render();
-};
-
-async function setupOpenDropdown(user: UserEvent, onChange?: () => void, current?: string) {
- const dropdown = setup(onChange, current);
+async function setupOpenDropdown(user: UserEvent, props: DataSourceDropdownProps) {
+ const dropdown = render();
const searchBox = dropdown.container.querySelector('input');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
@@ -87,6 +81,10 @@ jest.mock('../../hooks', () => {
};
});
+beforeAll(() => {
+ window.HTMLElement.prototype.scrollIntoView = jest.fn();
+});
+
const getListMock = jest.fn();
const getInstanceSettingsMock = jest.fn();
beforeEach(() => {
@@ -96,7 +94,7 @@ beforeEach(() => {
describe('DataSourceDropdown', () => {
it('should render', () => {
- expect(() => setup()).not.toThrow();
+ expect(() => render()).not.toThrow();
});
describe('configuration', () => {
@@ -121,7 +119,6 @@ describe('DataSourceDropdown', () => {
current: mockDS1.name,
...filters,
};
- window.HTMLElement.prototype.scrollIntoView = jest.fn();
const dropdown = render();
const searchBox = dropdown.container.querySelector('input');
@@ -130,40 +127,74 @@ describe('DataSourceDropdown', () => {
expect(getListMock.mock.lastCall[0]).toEqual(filters);
});
+ it('should dispaly the current selected DS in the selector', async () => {
+ getInstanceSettingsMock.mockReturnValue(mockDS2);
+ render();
+ expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name);
+ expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible();
+ });
+
it('should display the current ds on top', async () => {
//Mock ds is set as current, it appears on top
getInstanceSettingsMock.mockReturnValue(mockDS1);
- await setupOpenDropdown(user, jest.fn(), mockDS1.name);
+ await setupOpenDropdown(user, { onChange: jest.fn(), current: mockDS1.name });
let cards = await screen.findAllByTestId('data-source-card');
expect(await findByText(cards[0], mockDS1.name, { selector: 'span' })).toBeInTheDocument();
//xMock ds is set as current, it appears on top
getInstanceSettingsMock.mockReturnValue(mockDS2);
- await setupOpenDropdown(user, jest.fn(), mockDS2.name);
+ await setupOpenDropdown(user, { onChange: jest.fn(), current: mockDS2.name });
cards = await screen.findAllByTestId('data-source-card');
expect(await findByText(cards[0], mockDS2.name, { selector: 'span' })).toBeInTheDocument();
});
+ it('should dispaly the default DS as selected when `current` is not set', async () => {
+ getInstanceSettingsMock.mockReturnValue(mockDS2);
+ render();
+ expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name);
+ expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible();
+ });
+
it('should get the sorting function using the correct parameters', async () => {
//The actual sorting is tested in utils.test but let's make sure we're calling getDataSourceCompareFn with the correct parameters
const spy = jest.spyOn(utils, 'getDataSourceCompareFn');
- await setupOpenDropdown(user);
+ await setupOpenDropdown(user, { onChange: jest.fn(), current: mockDS1 });
expect(spy.mock.lastCall).toEqual([mockDS1, [mockDS2.name], ['${foo}']]);
});
+
+ it('should disable the dropdown when `disabled` is true', () => {
+ render();
+ expect(screen.getByTestId('Select a data source')).toBeDisabled();
+ });
+
+ it('should assign the correct `id` to the input element to pair it with a label', () => {
+ render();
+ expect(screen.getByTestId('Select a data source')).toHaveAttribute('id', 'custom.input.id');
+ });
+
+ it('should not set the default DS when setting `noDefault` to true and `current` is not provided', () => {
+ render();
+ getListMock.mockClear();
+ getInstanceSettingsMock.mockClear();
+ // Doesn't try to get the default DS
+ expect(getListMock).not.toBeCalled();
+ expect(getInstanceSettingsMock).not.toBeCalled();
+ expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', 'Select a data source');
+ });
});
describe('interactions', () => {
const user = userEvent.setup();
it('should open when clicked', async () => {
- await setupOpenDropdown(user);
+ await setupOpenDropdown(user, { onChange: jest.fn() });
expect(await screen.findByText(mockDS1.name, { selector: 'span' })).toBeInTheDocument();
});
it('should call onChange when a data source is clicked', async () => {
const onChange = jest.fn();
- await setupOpenDropdown(user, onChange);
+ await setupOpenDropdown(user, { onChange });
await user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(onChange.mock.lastCall[0]['name']).toEqual(mockDS2.name);
@@ -172,7 +203,7 @@ describe('DataSourceDropdown', () => {
it('should push recently used datasources when a data source is clicked', async () => {
const onChange = jest.fn();
- await setupOpenDropdown(user, onChange);
+ await setupOpenDropdown(user, { onChange });
await user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(pushRecentlyUsedDataSourceMock.mock.lastCall[0]).toEqual(mockDS2);
@@ -180,7 +211,7 @@ describe('DataSourceDropdown', () => {
it('should be navigatable by keyboard', async () => {
const onChange = jest.fn();
- await setupOpenDropdown(user, onChange);
+ await setupOpenDropdown(user, { onChange });
await user.keyboard('[ArrowDown]');
//Arrow down, second item is selected
@@ -202,7 +233,7 @@ describe('DataSourceDropdown', () => {
});
it('should be searchable', async () => {
- await setupOpenDropdown(user);
+ await setupOpenDropdown(user, { onChange: jest.fn() });
await user.keyboard(mockDS2.name); //Search for xMockDS
@@ -220,7 +251,7 @@ describe('DataSourceDropdown', () => {
it('should call onChange with the default query when add csv is clicked', async () => {
config.featureToggles.editPanelCSVDragAndDrop = true;
const onChange = jest.fn();
- await setupOpenDropdown(user, onChange);
+ await setupOpenDropdown(user, { onChange });
await user.click(await screen.findByText('Add csv or spreadsheet'));
@@ -231,7 +262,6 @@ describe('DataSourceDropdown', () => {
it('should open the modal when open advanced is clicked', async () => {
const props = { onChange: jest.fn(), current: mockDS1.name };
- window.HTMLElement.prototype.scrollIntoView = jest.fn();
render(
diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
index 1faef407014..95dec73300c 100644
--- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
@@ -3,11 +3,12 @@ import { useDialog } from '@react-aria/dialog';
import { useOverlay } from '@react-aria/overlays';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
+import { Observable } from 'rxjs';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
-import { DataQuery, DataSourceJsonData } from '@grafana/schema';
+import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema';
import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui';
import config from 'app/core/config';
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
@@ -19,7 +20,6 @@ import { DataSourceList } from './DataSourceList';
import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo';
import { DataSourceModal } from './DataSourceModal';
import { applyMaxSize, maxSize } from './popperModifiers';
-import { PickerContentProps, DataSourceDropdownProps } from './types';
import { dataSourceLabel, matchDataSourceWithSearch } from './utils';
const INTERACTION_EVENT_NAME = 'dashboards_dspicker_clicked';
@@ -31,8 +31,40 @@ const INTERACTION_ITEM = {
CONFIG_NEW_DS_EMPTY_STATE: 'config_new_ds_empty_state',
};
+export interface DataSourceDropdownProps {
+ onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
+ current?: DataSourceInstanceSettings | string | DataSourceRef | null | undefined;
+ recentlyUsed?: string[];
+ hideTextValue?: boolean;
+ width?: number;
+ inputId?: string;
+ noDefault?: boolean;
+ disabled?: boolean;
+
+ // DS filters
+ tracing?: boolean;
+ mixed?: boolean;
+ dashboard?: boolean;
+ metrics?: boolean;
+ type?: string | string[];
+ annotations?: boolean;
+ variables?: boolean;
+ alerting?: boolean;
+ pluginId?: string;
+ logs?: boolean;
+}
+
export function DataSourceDropdown(props: DataSourceDropdownProps) {
- const { current, onChange, hideTextValue, width, ...restProps } = props;
+ const {
+ current,
+ onChange,
+ hideTextValue = false,
+ width,
+ inputId,
+ noDefault = false,
+ disabled = false,
+ ...restProps
+ } = props;
const [isOpen, setOpen] = useState(false);
const [inputHasFocus, setInputHasFocus] = useState(false);
@@ -44,6 +76,10 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
setOpen(true);
markerElement?.focus();
};
+ const currentDataSourceInstanceSettings = useDatasource(current);
+ const currentValue = Boolean(!current && noDefault) ? undefined : currentDataSourceInstanceSettings;
+ const prefixIcon =
+ filterTerm && isOpen ? : ;
const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
@@ -79,8 +115,6 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
onChange(grafanaDS, [defaultFileUploadQuery]);
};
- const currentDataSourceInstanceSettings = useDatasource(current);
-
const popper = usePopper(markerElement, selectorElement, {
placement: 'bottom-start',
modifiers: [
@@ -114,25 +148,20 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
);
const { dialogProps } = useDialog({}, ref);
- const styles = useStyles2(getStylesDropdown);
+ const styles = useStyles2((theme: GrafanaTheme2) => getStylesDropdown(theme, props));
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 */}