mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
d87c2c4049
commit
6ad9e386ad
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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'));
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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}
|
||||||
|
@ -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(
|
||||||
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user