DS Picker: Filter available DS based on component props (#70613)

* Apply filters consistently to every list in the picker

* Display all built-in DS when editing a panel

* Add `uploadFile` prop to toggle the CSV file DS
This commit is contained in:
Ivan Ortega Alba 2023-07-06 11:50:55 +02:00 committed by GitHub
parent d87c2c4049
commit 6ad9e386ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 240 additions and 79 deletions

View File

@ -2,8 +2,8 @@
// however there are many cases where your component may not need an aria-label // however there are many cases where your component may not need an aria-label
// (a <button> with clear text, for example, does not need an aria-label as it's already labeled) // (a <button> with clear text, for example, does not need an aria-label as it's already labeled)
// but you still might need to select it for testing, // but you still might need to select it for testing,
// in that case please add the attribute data-test-id={selector} in the component and // in that case please add the attribute data-testid={selector} in the component and
// prefix your selector string with 'data-test-id' so that when create the selectors we know to search for it on the right attribute // prefix your selector string with 'data-testid' so that when create the selectors we know to search for it on the right attribute
/** /**
* Selectors grouped/defined in Components * Selectors grouped/defined in Components
* *
@ -78,7 +78,7 @@ export const Components = {
headerCornerInfo: (mode: string) => `Panel header ${mode}`, headerCornerInfo: (mode: string) => `Panel header ${mode}`,
loadingBar: () => `Panel loading bar`, loadingBar: () => `Panel loading bar`,
HoverWidget: { HoverWidget: {
container: 'data-test-id hover-header-container', container: 'data-testid hover-header-container',
dragIcon: 'data-testid drag-icon', dragIcon: 'data-testid drag-icon',
}, },
}, },

View File

@ -259,7 +259,7 @@ export function FileDropzoneDefaultChildren({ primaryText = 'Drop file here or c
const styles = getStyles(theme); const styles = getStyles(theme);
return ( return (
<div className={cx(styles.defaultDropZone)}> <div className={cx(styles.defaultDropZone)} data-testid="file-drop-zone-default-children">
<Icon className={cx(styles.icon)} name="upload" size="xl" /> <Icon className={cx(styles.icon)} name="upload" size="xl" />
<h6 className={cx(styles.primaryText)}>{primaryText}</h6> <h6 className={cx(styles.primaryText)}>{primaryText}</h6>
<small className={styles.small}>{secondaryText}</small> <small className={styles.small}>{secondaryText}</small>

View File

@ -20,9 +20,9 @@ describe('TopSearchBarSection', () => {
matches: true, matches: true,
})); }));
const { container } = renderComponent(); const component = renderComponent();
expect(container.querySelector('[data-test-id="wrapper"]')).toBeInTheDocument(); expect(component.queryByTestId('wrapper')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument();
}); });
@ -33,9 +33,9 @@ describe('TopSearchBarSection', () => {
matches: false, matches: false,
})); }));
const { container } = renderComponent(); const component = renderComponent();
expect(container.querySelector('[data-test-id="wrapper"]')).not.toBeInTheDocument(); expect(component.queryByTestId('wrapper')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument();
}); });
}); });

View File

@ -29,7 +29,7 @@ export function TopSearchBarSection({ children, align = 'left' }: TopSearchBarSe
} }
return ( return (
<div data-test-id="wrapper" className={cx(styles.wrapper, { [styles[align]]: align === 'right' })}> <div data-testid="wrapper" className={cx(styles.wrapper, { [styles[align]]: align === 'right' })}>
{children} {children}
</div> </div>
); );

View File

@ -18,16 +18,55 @@ interface BuiltInDataSourceListProps {
className?: string; className?: string;
current: DataSourceRef | string | null | undefined; current: DataSourceRef | string | null | undefined;
onChange: (ds: DataSourceInstanceSettings) => void; onChange: (ds: DataSourceInstanceSettings) => void;
dashboard?: boolean;
// DS filters
filter?: (ds: DataSourceInstanceSettings) => boolean;
tracing?: boolean;
mixed?: boolean; mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
type?: string | string[];
annotations?: boolean;
variables?: boolean;
alerting?: boolean;
pluginId?: string;
logs?: boolean;
} }
export function BuiltInDataSourceList({ className, current, onChange, dashboard, mixed }: BuiltInDataSourceListProps) { export function BuiltInDataSourceList({
const grafanaDataSources = useDatasources({ mixed, dashboard, filter: (ds) => !!ds.meta.builtIn }); className,
current,
onChange,
tracing,
dashboard,
mixed,
metrics,
type,
annotations,
variables,
alerting,
pluginId,
logs,
filter,
}: BuiltInDataSourceListProps) {
const grafanaDataSources = useDatasources({
tracing,
dashboard,
mixed,
metrics,
type,
annotations,
variables,
alerting,
pluginId,
logs,
});
const filteredResults = grafanaDataSources.filter((ds) => (filter ? filter?.(ds) : true) && !!ds.meta.builtIn);
return ( return (
<div className={className} data-testid="built-in-data-sources-list"> <div className={className} data-testid="built-in-data-sources-list">
{grafanaDataSources.map((ds) => { {filteredResults.map((ds) => {
return ( return (
<DataSourceCard <DataSourceCard
key={ds.uid} key={ds.uid}

View File

@ -100,7 +100,7 @@ describe('DataSourceDropdown', () => {
describe('configuration', () => { describe('configuration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
it('should call the dataSourceSrv.getDatasourceList with the correct filters', async () => { it('should fetch the DS applying the correct filters consistently across lists', async () => {
const filters = { const filters = {
mixed: true, mixed: true,
tracing: true, tracing: true,
@ -119,12 +119,27 @@ describe('DataSourceDropdown', () => {
current: mockDS1.name, current: mockDS1.name,
...filters, ...filters,
}; };
const dropdown = render(<DataSourceDropdown {...props}></DataSourceDropdown>);
const searchBox = dropdown.container.querySelector('input'); render(
<ModalsProvider>
<DataSourceDropdown {...props}></DataSourceDropdown>
<ModalRoot />
</ModalsProvider>
);
const searchBox = await screen.findByRole('textbox');
expect(searchBox).toBeInTheDocument(); expect(searchBox).toBeInTheDocument();
getListMock.mockClear();
await user.click(searchBox!); await user.click(searchBox!);
expect(getListMock.mock.lastCall[0]).toEqual(filters); await user.click(await screen.findByText('Open advanced data source picker'));
expect(await screen.findByText('Select data source')); //Data source modal is open
// Every call to the service must contain same filters
getListMock.mock.calls.forEach((call) =>
expect(call[0]).toMatchObject({
...filters,
})
);
}); });
it('should dispaly the current selected DS in the selector', async () => { it('should dispaly the current selected DS in the selector', async () => {
@ -180,7 +195,7 @@ describe('DataSourceDropdown', () => {
// Doesn't try to get the default DS // Doesn't try to get the default DS
expect(getListMock).not.toBeCalled(); expect(getListMock).not.toBeCalled();
expect(getInstanceSettingsMock).not.toBeCalled(); expect(getInstanceSettingsMock).not.toBeCalled();
expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', 'Select a data source'); expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', 'Select data source');
}); });
}); });
@ -251,7 +266,7 @@ describe('DataSourceDropdown', () => {
it('should call onChange with the default query when add csv is clicked', async () => { it('should call onChange with the default query when add csv is clicked', async () => {
config.featureToggles.editPanelCSVDragAndDrop = true; config.featureToggles.editPanelCSVDragAndDrop = true;
const onChange = jest.fn(); const onChange = jest.fn();
await setupOpenDropdown(user, { onChange }); await setupOpenDropdown(user, { onChange, uploadFile: true });
await user.click(await screen.findByText('Add csv or spreadsheet')); await user.click(await screen.findByText('Add csv or spreadsheet'));

View File

@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema'; import { DataQuery, DataSourceRef } from '@grafana/schema';
import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui'; import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui';
import config from 'app/core/config'; import config from 'app/core/config';
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection'; import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
@ -32,14 +32,15 @@ const INTERACTION_ITEM = {
}; };
export interface DataSourceDropdownProps { export interface DataSourceDropdownProps {
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void; onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current?: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined; current?: DataSourceInstanceSettings | string | DataSourceRef | null;
recentlyUsed?: string[]; recentlyUsed?: string[];
hideTextValue?: boolean; hideTextValue?: boolean;
width?: number; width?: number;
inputId?: string; inputId?: string;
noDefault?: boolean; noDefault?: boolean;
disabled?: boolean; disabled?: boolean;
placeholder?: string;
// DS filters // DS filters
tracing?: boolean; tracing?: boolean;
@ -52,6 +53,8 @@ export interface DataSourceDropdownProps {
alerting?: boolean; alerting?: boolean;
pluginId?: string; pluginId?: string;
logs?: boolean; logs?: boolean;
uploadFile?: boolean;
filter?: (ds: DataSourceInstanceSettings) => boolean;
} }
export function DataSourceDropdown(props: DataSourceDropdownProps) { export function DataSourceDropdown(props: DataSourceDropdownProps) {
@ -63,6 +66,7 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
inputId, inputId,
noDefault = false, noDefault = false,
disabled = false, disabled = false,
placeholder = 'Select data source',
...restProps ...restProps
} = props; } = props;
@ -161,7 +165,7 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
data-testid={selectors.components.DataSourcePicker.inputV2} data-testid={selectors.components.DataSourcePicker.inputV2}
prefix={currentValue ? prefixIcon : undefined} prefix={currentValue ? prefixIcon : undefined}
suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />} suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />}
placeholder={hideTextValue ? '' : dataSourceLabel(currentValue)} placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder}
onClick={openDropdown} onClick={openDropdown}
onFocus={() => { onFocus={() => {
setInputHasFocus(true); setInputHasFocus(true);
@ -196,10 +200,7 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
<PickerContent <PickerContent
keyboardEvents={keyboardEvents} keyboardEvents={keyboardEvents}
filterTerm={filterTerm} filterTerm={filterTerm}
onChange={( onChange={(ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
ds: DataSourceInstanceSettings<DataSourceJsonData>,
defaultQueries?: DataQuery[] | GrafanaQuery[]
) => {
onClose(); onClose();
onChange(ds, defaultQueries); onChange(ds, defaultQueries);
}} }}
@ -248,9 +249,9 @@ export interface PickerContentProps extends DataSourceDropdownProps {
} }
const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((props, ref) => { const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((props, ref) => {
const { filterTerm, onChange, onClose, onClickAddCSV, current } = props; const { filterTerm, onChange, onClose, onClickAddCSV, current, filter, uploadFile } = props;
const changeCallback = useCallback( const changeCallback = useCallback(
(ds: DataSourceInstanceSettings<DataSourceJsonData>) => { (ds: DataSourceInstanceSettings) => {
onChange(ds); onChange(ds);
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SELECT_DS, ds_type: ds.type }); reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SELECT_DS, ds_type: ds.type });
}, },
@ -274,7 +275,7 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
className={styles.dataSourceList} className={styles.dataSourceList}
current={current} current={current}
onChange={changeCallback} onChange={changeCallback}
filter={(ds) => matchDataSourceWithSearch(ds, filterTerm)} filter={(ds) => (filter ? filter?.(ds) : true) && matchDataSourceWithSearch(ds, filterTerm)}
onClickEmptyStateCTA={() => onClickEmptyStateCTA={() =>
reportInteraction(INTERACTION_EVENT_NAME, { reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE, item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE,
@ -293,9 +294,19 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
onClose(); onClose();
showModal(DataSourceModal, { showModal(DataSourceModal, {
reportedInteractionFrom: 'ds_picker', reportedInteractionFrom: 'ds_picker',
tracing: props.tracing,
dashboard: props.dashboard, dashboard: props.dashboard,
mixed: props.mixed, mixed: props.mixed,
current, metrics: props.metrics,
type: props.type,
annotations: props.annotations,
variables: props.variables,
alerting: props.alerting,
pluginId: props.pluginId,
logs: props.logs,
filter: props.filter,
uploadFile: props.uploadFile,
current: props.current,
onDismiss: hideModal, onDismiss: hideModal,
onChange: (ds, defaultQueries) => { onChange: (ds, defaultQueries) => {
onChange(ds, defaultQueries); onChange(ds, defaultQueries);
@ -310,7 +321,7 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
</Button> </Button>
)} )}
</ModalsController> </ModalsController>
{onClickAddCSV && config.featureToggles.editPanelCSVDragAndDrop && ( {uploadFile && config.featureToggles.editPanelCSVDragAndDrop && (
<Button variant="secondary" size="sm" onClick={clickAddCSVCallback}> <Button variant="secondary" size="sm" onClick={clickAddCSVCallback}>
Add csv or spreadsheet Add csv or spreadsheet
</Button> </Button>

View File

@ -1,11 +1,11 @@
import { findByText, queryByText, render, screen } from '@testing-library/react'; import { queryByTestId, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data'; import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
import { config, GetDataSourceListFilters } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { DataSourceModal } from './DataSourceModal'; import { DataSourceModal, DataSourceModalProps } from './DataSourceModal';
const pluginMetaInfo: PluginMetaInfo = { const pluginMetaInfo: PluginMetaInfo = {
author: { name: '' }, author: { name: '' },
@ -40,10 +40,17 @@ const mockDSBuiltIn = createDS('mock.datasource.builtin', 3, true);
const mockDSList = [mockDS1, mockDS2, mockDSBuiltIn]; const mockDSList = [mockDS1, mockDS2, mockDSBuiltIn];
const setup = (onChange = () => {}, onDismiss = () => {}) => { const setup = (partialProps: Partial<DataSourceModalProps> = {}) => {
const props = { onChange, onDismiss, current: mockDS1.name };
window.HTMLElement.prototype.scrollIntoView = function () {}; window.HTMLElement.prototype.scrollIntoView = function () {};
return render(<DataSourceModal {...props}></DataSourceModal>);
const props: DataSourceModalProps = {
...partialProps,
onChange: partialProps.onChange || jest.fn(),
onDismiss: partialProps.onDismiss || jest.fn(),
current: partialProps.current || mockDS1,
};
return render(<DataSourceModal {...props} />);
}; };
jest.mock('@grafana/runtime', () => { jest.mock('@grafana/runtime', () => {
@ -61,17 +68,19 @@ jest.mock('@grafana/runtime', () => {
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return { return {
getDataSourceSrv: () => ({ getDataSourceSrv: () => ({
getList: (filters: GetDataSourceListFilters) => { getList: getListMock,
if (filters.filter) { getInstanceSettings: getInstanceSettingsMock,
return mockDSList.filter(filters.filter);
}
return mockDSList;
},
getInstanceSettings: () => mockDS1,
}), }),
}; };
}); });
const getListMock = jest.fn();
const getInstanceSettingsMock = jest.fn();
beforeEach(() => {
getListMock.mockReturnValue(mockDSList);
getInstanceSettingsMock.mockReturnValue(mockDS1);
});
describe('DataSourceDropdown', () => { describe('DataSourceDropdown', () => {
it('should render', () => { it('should render', () => {
expect(() => setup()).not.toThrow(); expect(() => setup()).not.toThrow();
@ -90,24 +99,74 @@ describe('DataSourceDropdown', () => {
}); });
it('only displays the file drop area when the the ff is enabled', async () => { it('only displays the file drop area when the the ff is enabled', async () => {
const defaultValue = config.featureToggles.editPanelCSVDragAndDrop;
config.featureToggles.editPanelCSVDragAndDrop = true; config.featureToggles.editPanelCSVDragAndDrop = true;
setup(); setup({ uploadFile: true });
expect(await screen.findByText('Drop file here or click to upload')).toBeInTheDocument();
config.featureToggles.editPanelCSVDragAndDrop = false; expect(await screen.queryByTestId('file-drop-zone-default-children')).toBeInTheDocument();
config.featureToggles.editPanelCSVDragAndDrop = defaultValue;
}); });
it('does not show the file drop area when the ff is disabled', async () => { it('does not show the file drop area when the ff is disabled', async () => {
setup(); const defaultValue = config.featureToggles.editPanelCSVDragAndDrop;
expect(screen.queryByText('Drop file here or click to upload')).toBeNull(); config.featureToggles.editPanelCSVDragAndDrop = false;
setup({ uploadFile: true });
expect(await screen.queryByTestId('file-drop-zone-default-children')).toBeNull();
config.featureToggles.editPanelCSVDragAndDrop = defaultValue;
}); });
it('should only display built in datasources in the right column', async () => { it('should not display the drop zone by default', async () => {
setup(); const defaultValue = config.featureToggles.editPanelCSVDragAndDrop;
const dsList = await screen.findByTestId('data-sources-list'); config.featureToggles.editPanelCSVDragAndDrop = true;
const builtInDSList = (await screen.findAllByTestId('built-in-data-sources-list'))[1]; //The second element needs to be selected as the first element is the one on the left, under the regular data sources.
expect(queryByText(dsList, mockDSBuiltIn.name)).toBeNull(); const component = setup();
expect(await findByText(builtInDSList, mockDSBuiltIn.name, { selector: 'span' })).toBeInTheDocument();
expect(queryByTestId(component.container, 'file-drop-zone-default-children')).toBeNull();
config.featureToggles.editPanelCSVDragAndDrop = defaultValue;
});
it('should display the drop zone when uploadFile is enabled', async () => {
const defaultValue = config.featureToggles.editPanelCSVDragAndDrop;
config.featureToggles.editPanelCSVDragAndDrop = true;
setup({ uploadFile: true });
expect(await screen.queryByTestId('file-drop-zone-default-children')).toBeInTheDocument();
config.featureToggles.editPanelCSVDragAndDrop = defaultValue;
});
it('should fetch the DS applying the correct filters consistently across lists', async () => {
const filters = {
mixed: true,
tracing: true,
dashboard: true,
metrics: true,
type: 'foo',
annotations: true,
variables: true,
alerting: true,
pluginId: 'pluginid',
logs: true,
};
const props = {
onChange: () => {},
onDismiss: () => {},
current: mockDS1.name,
...filters,
};
getListMock.mockClear();
render(<DataSourceModal {...props}></DataSourceModal>);
// Every call to the service must contain same filters
expect(getListMock).toHaveBeenCalled();
getListMock.mock.calls.forEach((call) =>
expect(call[0]).toMatchObject({
...filters,
})
);
}); });
}); });
@ -134,9 +193,10 @@ describe('DataSourceDropdown', () => {
const user = userEvent.setup(); const user = userEvent.setup();
config.featureToggles.editPanelCSVDragAndDrop = true; config.featureToggles.editPanelCSVDragAndDrop = true;
const onChange = jest.fn(); const onChange = jest.fn();
setup(onChange); setup({ onChange, uploadFile: true });
const fileInput = ( const fileInput = (
await screen.findByText('Drop file here or click to upload') await screen.queryByTestId('file-drop-zone-default-children')!
).parentElement!.parentElement!.querySelector('input'); ).parentElement!.parentElement!.querySelector('input');
const file = new File([''], 'test.csv', { type: 'text/plain' }); const file = new File([''], 'test.csv', { type: 'text/plain' });
expect(fileInput).toBeInTheDocument(); expect(fileInput).toBeInTheDocument();
@ -154,7 +214,7 @@ describe('DataSourceDropdown', () => {
it('should call the onChange handler with the correct datasource', async () => { it('should call the onChange handler with the correct datasource', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onChange = jest.fn(); const onChange = jest.fn();
setup(onChange); setup({ onChange });
await user.click(await screen.findByText(mockDS2.name, { selector: 'span' })); await user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(onChange.mock.lastCall[0].name).toEqual(mockDS2.name); expect(onChange.mock.lastCall[0].name).toEqual(mockDS2.name);
}); });

View File

@ -35,7 +35,7 @@ const INTERACTION_ITEM = {
DISMISS: 'dismiss', DISMISS: 'dismiss',
}; };
interface DataSourceModalProps { export interface DataSourceModalProps {
onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void; onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current: DataSourceRef | string | null | undefined; current: DataSourceRef | string | null | undefined;
onDismiss: () => void; onDismiss: () => void;
@ -43,6 +43,7 @@ interface DataSourceModalProps {
reportedInteractionFrom?: string; reportedInteractionFrom?: string;
// DS filters // DS filters
filter?: (ds: DataSourceInstanceSettings) => boolean;
tracing?: boolean; tracing?: boolean;
mixed?: boolean; mixed?: boolean;
dashboard?: boolean; dashboard?: boolean;
@ -53,11 +54,22 @@ interface DataSourceModalProps {
alerting?: boolean; alerting?: boolean;
pluginId?: string; pluginId?: string;
logs?: boolean; logs?: boolean;
uploadFile?: boolean;
} }
export function DataSourceModal({ export function DataSourceModal({
tracing,
dashboard, dashboard,
mixed, mixed,
metrics,
type,
annotations,
variables,
alerting,
pluginId,
logs,
uploadFile,
filter,
onChange, onChange,
current, current,
onDismiss, onDismiss,
@ -106,6 +118,29 @@ export function DataSourceModal({
} }
}); });
// Built-in data sources used twice because of mobile layout adjustments
// In movile the list is appended to the bottom of the DS list
const BuiltInList = ({ className }: { className?: string }) => {
return (
<BuiltInDataSourceList
className={className}
onChange={onChangeDataSource}
current={current}
filter={filter}
variables={variables}
tracing={tracing}
metrics={metrics}
type={type}
annotations={annotations}
alerting={alerting}
pluginId={pluginId}
logs={logs}
dashboard={dashboard}
mixed={mixed}
/>
);
};
return ( return (
<Modal <Modal
title="Select data source" title="Select data source"
@ -132,10 +167,6 @@ export function DataSourceModal({
/> />
<CustomScrollbar> <CustomScrollbar>
<DataSourceList <DataSourceList
dashboard={false}
mixed={false}
variables
filter={(ds) => matchDataSourceWithSearch(ds, search) && !ds.meta.builtIn}
onChange={onChangeDataSource} onChange={onChangeDataSource}
current={current} current={current}
onClickEmptyStateCTA={() => onClickEmptyStateCTA={() =>
@ -144,27 +175,27 @@ export function DataSourceModal({
src: analyticsInteractionSrc, src: analyticsInteractionSrc,
}) })
} }
/> filter={(ds) => (filter ? filter?.(ds) : true) && matchDataSourceWithSearch(ds, search) && !ds.meta.builtIn}
<BuiltInDataSourceList variables={variables}
tracing={tracing}
metrics={metrics}
type={type}
annotations={annotations}
alerting={alerting}
pluginId={pluginId}
logs={logs}
dashboard={dashboard} dashboard={dashboard}
mixed={mixed} mixed={mixed}
className={styles.appendBuiltInDataSourcesList}
onChange={onChangeDataSource}
current={current}
/> />
<BuiltInList className={styles.appendBuiltInDataSourcesList} />
</CustomScrollbar> </CustomScrollbar>
</div> </div>
<div className={styles.rightColumn}> <div className={styles.rightColumn}>
<div className={styles.builtInDataSources}> <div className={styles.builtInDataSources}>
<CustomScrollbar className={styles.builtInDataSourcesList}> <CustomScrollbar className={styles.builtInDataSourcesList}>
<BuiltInDataSourceList <BuiltInList />
onChange={onChangeDataSource}
current={current}
dashboard={dashboard}
mixed={mixed}
/>
</CustomScrollbar> </CustomScrollbar>
{config.featureToggles.editPanelCSVDragAndDrop && ( {uploadFile && config.featureToggles.editPanelCSVDragAndDrop && (
<FileDropzone <FileDropzone
readAs="readAsArrayBuffer" readAs="readAsArrayBuffer"
fileListRenderer={() => undefined} fileListRenderer={() => undefined}

View File

@ -20,7 +20,7 @@ export function dataSourceLabel(
dataSource: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined dataSource: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined
) { ) {
if (!dataSource) { if (!dataSource) {
return 'Select a data source'; return undefined;
} }
if (typeof dataSource === 'string') { if (typeof dataSource === 'string') {
@ -35,7 +35,7 @@ export function dataSourceLabel(
return `${dataSource.uid} - not found`; return `${dataSource.uid} - not found`;
} }
return 'Select a data source'; return undefined;
} }
export function getDataSourceCompareFn( export function getDataSourceCompareFn(

View File

@ -272,7 +272,12 @@ export class QueryGroup extends PureComponent<Props, State> {
const { isDataSourceModalOpen } = this.state; const { isDataSourceModalOpen } = this.state;
const commonProps = { const commonProps = {
metrics: true,
mixed: true,
dashboard: true,
variables: true,
current: this.props.options.dataSource, current: this.props.options.dataSource,
uploadFile: true,
onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => { onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
await this.onChangeDataSource(ds, defaultQueries); await this.onChangeDataSource(ds, defaultQueries);
this.onCloseDataSourceModal(); this.onCloseDataSourceModal();
@ -284,7 +289,7 @@ export class QueryGroup extends PureComponent<Props, State> {
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && ( {isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
<DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal> <DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal>
)} )}
<DataSourcePicker {...commonProps} metrics={true} mixed={true} dashboard={true} variables={true} /> <DataSourcePicker {...commonProps} />
</> </>
); );
}; };