DataSourcePicker: refactor file drop out of query group (#68236)

* refactor file drop out of query group

* make sure we display errors when file upload fails

* refactor to make onChange take default queries

* let grafana datasource handle file -> query

* add dropdown tests

* add modal tests

* add filtering props to dropdown

---------

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
Oscar Kilhed 2023-06-21 10:55:55 +02:00 committed by GitHub
parent d65c9396f3
commit cf4d86d95f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 520 additions and 103 deletions

View File

@ -26,7 +26,7 @@ export function BuiltInDataSourceList({ className, current, onChange, dashboard,
const grafanaDataSources = useDatasources({ mixed, dashboard, filter: (ds) => !!ds.meta.builtIn });
return (
<div className={className}>
<div className={className} data-testid="built-in-data-sources-list">
{grafanaDataSources.map((ds) => {
return (
<DataSourceCard

View File

@ -0,0 +1,254 @@
import { findByText, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import React from 'react';
import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
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 * as utils from './utils';
const pluginMetaInfo: PluginMetaInfo = {
author: { name: '' },
description: '',
screenshots: [],
version: '',
updated: '',
links: [],
logos: { small: '', large: '' },
};
function createPluginMeta(name: string, builtIn: boolean): DataSourcePluginMeta {
return { builtIn, name, id: name, type: PluginType.datasource, baseUrl: '', info: pluginMetaInfo, module: '' };
}
function createDS(name: string, id: number, builtIn: boolean): DataSourceInstanceSettings {
return {
name: name,
uid: name + 'uid',
meta: createPluginMeta(name, builtIn),
id,
access: 'direct',
jsonData: {},
type: '',
readOnly: true,
};
}
const mockDS1 = createDS('mock.datasource.1', 1, false);
const mockDS2 = createDS('mock.datasource.2', 2, false);
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(<DataSourceDropdown {...props}></DataSourceDropdown>);
};
async function setupOpenDropdown(user: UserEvent, onChange?: () => void, current?: string) {
const dropdown = setup(onChange, current);
const searchBox = dropdown.container.querySelector('input');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
}
jest.mock('@grafana/runtime', () => {
const actual = jest.requireActual('@grafana/runtime');
return {
...actual,
getTemplateSrv: () => {
return {
getVariables: () => [{ id: 'foo', type: 'datasource' }],
};
},
};
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
getList: getListMock,
getInstanceSettings: getInstanceSettingsMock,
}),
};
});
const pushRecentlyUsedDataSourceMock = jest.fn();
jest.mock('../../hooks', () => {
const actual = jest.requireActual('../../hooks');
return {
...actual,
useRecentlyUsedDataSources: () => [[mockDS2.name], pushRecentlyUsedDataSourceMock],
};
});
const getListMock = jest.fn();
const getInstanceSettingsMock = jest.fn();
beforeEach(() => {
getListMock.mockReturnValue(mockDSList);
getInstanceSettingsMock.mockReturnValue(mockDS1);
});
describe('DataSourceDropdown', () => {
it('should render', () => {
expect(() => setup()).not.toThrow();
});
describe('configuration', () => {
const user = userEvent.setup();
it('should call the dataSourceSrv.getDatasourceList with the correct filters', 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: () => {},
current: mockDS1.name,
...filters,
};
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const dropdown = render(<DataSourceDropdown {...props}></DataSourceDropdown>);
const searchBox = dropdown.container.querySelector('input');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
expect(getListMock.mock.lastCall[0]).toEqual(filters);
});
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);
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);
cards = await screen.findAllByTestId('data-source-card');
expect(await findByText(cards[0], mockDS2.name, { selector: 'span' })).toBeInTheDocument();
});
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);
expect(spy.mock.lastCall).toEqual([mockDS1, [mockDS2.name], ['${foo}']]);
});
});
describe('interactions', () => {
const user = userEvent.setup();
it('should open when clicked', async () => {
await setupOpenDropdown(user);
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 user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(onChange.mock.lastCall[0]['name']).toEqual(mockDS2.name);
expect(screen.queryByText(mockDS1.name, { selector: 'span' })).toBeNull();
});
it('should push recently used datasources when a data source is clicked', async () => {
const onChange = jest.fn();
await setupOpenDropdown(user, onChange);
await user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(pushRecentlyUsedDataSourceMock.mock.lastCall[0]).toEqual(mockDS2);
});
it('should be navigatable by keyboard', async () => {
const onChange = jest.fn();
await setupOpenDropdown(user, onChange);
await user.keyboard('[ArrowDown]');
//Arrow down, second item is selected
const xMockDSElement = getCard(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(xMockDSElement?.dataset.selecteditem).toEqual('true');
let mockDSElement = getCard(await screen.findByText(mockDS1.name, { selector: 'span' }));
expect(mockDSElement?.dataset.selecteditem).toEqual('false');
await user.keyboard('[ArrowUp]');
//Arrow up, first item is selected again
mockDSElement = getCard(await screen.findByText(mockDS1.name, { selector: 'span' }));
expect(mockDSElement?.dataset.selecteditem).toEqual('true');
await user.keyboard('[ArrowDown]');
await user.keyboard('[Enter]');
//Arrow down to navigate to xMock, enter to select it. Assert onChange called with correct DS and dropdown closed.
expect(onChange.mock.lastCall[0]['name']).toEqual(mockDS2.name);
expect(screen.queryByText(mockDS1.name, { selector: 'span' })).toBeNull();
});
it('should be searchable', async () => {
await setupOpenDropdown(user);
await user.keyboard(mockDS2.name); //Search for xMockDS
expect(screen.queryByText(mockDS1.name, { selector: 'span' })).toBeNull();
const xMockCard = getCard(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(xMockCard).toBeInTheDocument();
expect(xMockCard?.dataset.selecteditem).toEqual('true'); //The first search result is selected
await user.keyboard('foobarbaz'); //Search for a DS that should not exist
expect(await screen.findByText('Configure a new data source')).toBeInTheDocument();
});
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 user.click(await screen.findByText('Add csv or spreadsheet'));
expect(onChange.mock.lastCall[1]).toEqual([defaultFileUploadQuery]);
expect(screen.queryByText('Open advanced data source picker')).toBeNull(); //Drop down is closed
config.featureToggles.editPanelCSVDragAndDrop = false;
});
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(
<ModalsProvider>
<DataSourceDropdown {...props}></DataSourceDropdown>
<ModalRoot />
</ModalsProvider>
);
const searchBox = await screen.findByRole('textbox');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
await user.click(await screen.findByText('Open advanced data source picker'));
expect(await screen.findByText('Select data source')); //Data source modal is open
expect(screen.queryByText('Open advanced data source picker')).toBeNull(); //Drop down is closed
});
});
});
function getCard(element: HTMLElement) {
return element.parentElement?.parentElement?.parentElement?.parentElement;
}

View File

@ -7,10 +7,11 @@ import { usePopper } from 'react-popper';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { DataSourceJsonData } from '@grafana/schema';
import { DataQuery, DataSourceJsonData } 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';
import { defaultFileUploadQuery, GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { useDatasource } from '../../hooks';
@ -68,6 +69,15 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
});
return () => sub.unsubscribe();
});
const grafanaDS = useDatasource('-- Grafana --');
const onClickAddCSV = () => {
if (!grafanaDS) {
return;
}
onChange(grafanaDS, [defaultFileUploadQuery]);
};
const currentDataSourceInstanceSettings = useDatasource(current);
@ -154,14 +164,18 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
<PickerContent
keyboardEvents={keyboardEvents}
filterTerm={filterTerm}
onChange={(ds: DataSourceInstanceSettings<DataSourceJsonData>) => {
onChange={(
ds: DataSourceInstanceSettings<DataSourceJsonData>,
defaultQueries?: DataQuery[] | GrafanaQuery[]
) => {
onClose();
onChange(ds);
onChange(ds, defaultQueries);
}}
onClose={onClose}
current={currentDataSourceInstanceSettings}
style={popper.styles.popper}
ref={setSelectorElement}
onClickAddCSV={onClickAddCSV}
{...restProps}
onDismiss={onClose}
{...popper.attributes.popper}
@ -237,15 +251,13 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
onClick={() => {
onClose();
showModal(DataSourceModal, {
enableFileUpload: props.enableFileUpload,
fileUploadOptions: props.fileUploadOptions,
reportedInteractionFrom: 'ds_picker',
dashboard: props.dashboard,
mixed: props.mixed,
current,
onDismiss: hideModal,
onChange: (ds) => {
onChange(ds);
onChange: (ds, defaultQueries) => {
onChange(ds, defaultQueries);
hideModal();
},
});

View File

@ -71,7 +71,7 @@ export function DataSourceList(props: DataSourceListProps) {
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
return (
<div ref={containerRef} className={cx(className, styles.container)}>
<div ref={containerRef} className={cx(className, styles.container)} data-testid="data-sources-list">
{filteredDataSources.length === 0 && (
<EmptyState className={styles.emptyState} onClickCTA={onClickEmptyStateCTA} />
)}
@ -79,6 +79,7 @@ export function DataSourceList(props: DataSourceListProps) {
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
.map((ds) => (
<DataSourceCard
data-testid="data-source-card"
key={ds.uid}
ds={ds}
onClick={() => {

View File

@ -0,0 +1,159 @@
import { findByText, queryByText, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
import { config, GetDataSourceListFilters } from '@grafana/runtime';
import { DataSourceModal } from './DataSourceModal';
const pluginMetaInfo: PluginMetaInfo = {
author: { name: '' },
description: '',
screenshots: [],
version: '',
updated: '',
links: [],
logos: { small: '', large: '' },
};
function createPluginMeta(name: string, builtIn: boolean): DataSourcePluginMeta {
return { builtIn, name, id: name, type: PluginType.datasource, baseUrl: '', info: pluginMetaInfo, module: '' };
}
function createDS(name: string, id: number, builtIn: boolean): DataSourceInstanceSettings {
return {
name: name,
uid: name + 'uid',
meta: createPluginMeta(name, builtIn),
id,
access: 'direct',
jsonData: {},
type: '',
readOnly: true,
};
}
const mockDS1 = createDS('mock.datasource.1', 1, false);
const mockDS2 = createDS('mock.datasource.2', 2, false);
const mockDSBuiltIn = createDS('mock.datasource.builtin', 3, true);
const mockDSList = [mockDS1, mockDS2, mockDSBuiltIn];
const setup = (onChange = () => {}, onDismiss = () => {}) => {
const props = { onChange, onDismiss, current: mockDS1.name };
window.HTMLElement.prototype.scrollIntoView = function () {};
return render(<DataSourceModal {...props}></DataSourceModal>);
};
jest.mock('@grafana/runtime', () => {
const actual = jest.requireActual('@grafana/runtime');
return {
...actual,
getTemplateSrv: () => {
return {
getVariables: () => [],
};
},
};
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
getList: (filters: GetDataSourceListFilters) => {
if (filters.filter) {
return mockDSList.filter(filters.filter);
}
return mockDSList;
},
getInstanceSettings: () => mockDS1,
}),
};
});
describe('DataSourceDropdown', () => {
it('should render', () => {
expect(() => setup()).not.toThrow();
});
describe('configuration', () => {
const user = userEvent.setup();
it('displays the configure new datasource when the list is empty', async () => {
setup();
const searchBox = await screen.findByRole('searchbox');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
await user.keyboard('foobarbaz'); //Search for a DS that should not exist
expect(screen.queryAllByText('Configure a new data source')).toHaveLength(2);
});
it('only displays the file drop area when the the ff is enabled', async () => {
config.featureToggles.editPanelCSVDragAndDrop = true;
setup();
expect(await screen.findByText('Drop file here or click to upload')).toBeInTheDocument();
config.featureToggles.editPanelCSVDragAndDrop = false;
});
it('does not show the file drop area when the ff is disabled', async () => {
setup();
expect(screen.queryByText('Drop file here or click to upload')).toBeNull();
});
it('should only display built in datasources in the right column', async () => {
setup();
const dsList = await screen.findByTestId('data-sources-list');
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();
expect(await findByText(builtInDSList, mockDSBuiltIn.name, { selector: 'span' })).toBeInTheDocument();
});
});
describe('interactions', () => {
const user = userEvent.setup();
it('should be searchable', async () => {
setup();
const searchBox = await screen.findByRole('searchbox');
expect(searchBox).toBeInTheDocument();
await user.click(searchBox!);
await user.keyboard(mockDS2.name); //Search for xMockDS
expect(screen.queryByText(mockDS1.name, { selector: 'span' })).toBeNull();
expect(await screen.findByText(mockDS2.name, { selector: 'span' })).toBeInTheDocument();
await user.keyboard('foobarbaz'); //Search for a DS that should not exist
expect(await screen.findByText('No data sources found')).toBeInTheDocument();
});
it('calls the onChange with the default query containing the file', async () => {
config.featureToggles.editPanelCSVDragAndDrop = true;
const onChange = jest.fn();
setup(onChange);
const fileInput = (
await screen.findByText('Drop file here or click to upload')
).parentElement!.parentElement!.querySelector('input');
const file = new File([''], 'test.csv', { type: 'text/plain' });
await user.upload(fileInput!, file);
const defaultQuery = onChange.mock.lastCall[1][0];
expect(defaultQuery).toMatchObject({
refId: 'A',
datasource: { type: 'grafana', uid: 'grafana' },
queryType: 'snapshot',
file: { path: 'test.csv' },
});
config.featureToggles.editPanelCSVDragAndDrop = false;
});
it('should call the onChange handler with the correct datasource', async () => {
const onChange = jest.fn();
setup(onChange);
await user.click(await screen.findByText(mockDS2.name, { selector: 'span' }));
expect(onChange.mock.lastCall[0].name).toEqual(mockDS2.name);
});
});
});

View File

@ -1,10 +1,10 @@
import { css } from '@emotion/css';
import { once } from 'lodash';
import React, { useState } from 'react';
import { DropzoneOptions } from 'react-dropzone';
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { config, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import {
Modal,
FileDropzone,
@ -15,6 +15,10 @@ import {
Icon,
} from '@grafana/ui';
import * as DFImport from 'app/features/dataframe-import';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { getFileDropToQueryHandler } from 'app/plugins/datasource/grafana/utils';
import { useDatasource } from '../../hooks';
import { AddNewDataSourceButton } from './AddNewDataSourceButton';
import { BuiltInDataSourceList } from './BuiltInDataSourceList';
@ -32,22 +36,18 @@ const INTERACTION_ITEM = {
};
interface DataSourceModalProps {
onChange: (ds: DataSourceInstanceSettings) => void;
onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current: DataSourceRef | string | null | undefined;
onDismiss: () => void;
recentlyUsed?: string[];
enableFileUpload?: boolean;
dashboard?: boolean;
mixed?: boolean;
fileUploadOptions?: DropzoneOptions;
reportedInteractionFrom?: string;
}
export function DataSourceModal({
enableFileUpload,
dashboard,
mixed,
fileUploadOptions,
onChange,
current,
onDismiss,
@ -78,6 +78,24 @@ export function DataSourceModal({
[analyticsInteractionSrc]
);
const grafanaDS = useDatasource('-- Grafana --');
const onFileDrop = getFileDropToQueryHandler((query, fileRejections) => {
if (!grafanaDS) {
return;
}
onChange(grafanaDS, [query]);
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.UPLOAD_FILE,
src: analyticsInteractionSrc,
});
if (fileRejections.length < 1) {
onDismiss();
}
});
return (
<Modal
title="Select data source"
@ -91,6 +109,7 @@ export function DataSourceModal({
>
<div className={styles.leftColumn}>
<Input
type="search"
autoFocus
className={styles.searchInput}
value={search}
@ -135,7 +154,7 @@ export function DataSourceModal({
mixed={mixed}
/>
</CustomScrollbar>
{enableFileUpload && (
{config.featureToggles.editPanelCSVDragAndDrop && (
<FileDropzone
readAs="readAsArrayBuffer"
fileListRenderer={() => undefined}
@ -143,15 +162,7 @@ export function DataSourceModal({
maxSize: DFImport.maxFileSize,
multiple: false,
accept: DFImport.acceptedFiles,
...fileUploadOptions,
onDrop: (...args) => {
fileUploadOptions?.onDrop?.(...args);
onDismiss();
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.UPLOAD_FILE,
src: analyticsInteractionSrc,
});
},
onDrop: onFileDrop,
}}
>
<FileDropzoneDefaultChildren />

View File

@ -1,24 +1,30 @@
import React from 'react';
import { DropzoneOptions } from 'react-dropzone';
import { Observable } from 'rxjs';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
export interface DataSourceDropdownProps {
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>) => void;
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
enableFileUpload?: boolean;
fileUploadOptions?: DropzoneOptions;
onClickAddCSV?: () => void;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
type?: string | string[];
annotations?: boolean;
variables?: boolean;
alerting?: boolean;
pluginId?: string;
logs?: boolean;
recentlyUsed?: string[];
hideTextValue?: boolean;
dashboard?: boolean;
mixed?: boolean;
width?: number;
}
export interface PickerContentProps extends DataSourceDropdownProps {
onClickAddCSV?: () => void;
keyboardEvents: Observable<React.KeyboardEvent>;
style: React.CSSProperties;
filterTerm?: string;

View File

@ -1,13 +1,9 @@
import { css } from '@emotion/css';
import React, { PureComponent } from 'react';
import { DropEvent, FileRejection } from 'react-dropzone';
import { Unsubscribable } from 'rxjs';
import {
CoreApp,
DataFrameJSON,
dataFrameToJSON,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
getDefaultTimeRange,
@ -16,17 +12,17 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getDataSourceSrv, locationService } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import config from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
import { addQuery, queryIsEmpty } from 'app/core/utils/query';
import * as DFImport from 'app/features/dataframe-import';
import { DataSourceModal } from 'app/features/datasources/components/picker/DataSourceModal';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { QueryGroupOptions } from 'app/types';
import { PanelQueryRunner } from '../state/PanelQueryRunner';
@ -136,13 +132,16 @@ export class QueryGroup extends PureComponent<Props, State> {
this.setState({ data });
}
onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => {
onChangeDataSource = async (
newSettings: DataSourceInstanceSettings,
defaultQueries?: DataQuery[] | GrafanaQuery[]
) => {
const { dsSettings } = this.state;
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
const nextDS = await getDataSourceSrv().get(newSettings.uid);
// We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
const queries = await updateQueries(nextDS, newSettings.uid, this.state.queries, currentDS);
const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, this.state.queries, currentDS));
const dataSource = await this.dataSourceSrv.get(newSettings.name);
@ -161,6 +160,10 @@ export class QueryGroup extends PureComponent<Props, State> {
dataSource: dataSource,
dsSettings: newSettings,
});
if (defaultQueries) {
this.props.onRunQueries();
}
};
onAddQueryClick = () => {
@ -269,16 +272,9 @@ export class QueryGroup extends PureComponent<Props, State> {
const { isDataSourceModalOpen } = this.state;
const commonProps = {
enableFileUpload: config.featureToggles.editPanelCSVDragAndDrop,
fileUploadOptions: {
onDrop: this.onFileDrop,
maxSize: DFImport.maxFileSize,
multiple: false,
accept: DFImport.acceptedFiles,
},
current: this.props.options.dataSource,
onChange: (ds: DataSourceInstanceSettings) => {
this.onChangeDataSource(ds);
onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
await this.onChangeDataSource(ds, defaultQueries);
this.onCloseDataSourceModal();
},
};
@ -288,14 +284,7 @@ export class QueryGroup extends PureComponent<Props, State> {
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
<DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal>
)}
<DataSourcePicker
{...commonProps}
metrics={true}
mixed={true}
dashboard={true}
variables={true}
onClickAddCSV={this.onClickAddCSV}
/>
<DataSourcePicker {...commonProps} metrics={true} mixed={true} dashboard={true} variables={true} />
</>
);
};
@ -306,49 +295,6 @@ export class QueryGroup extends PureComponent<Props, State> {
this.onScrollBottom();
};
onClickAddCSV = async () => {
const ds = getDataSourceSrv().getInstanceSettings('-- Grafana --');
await this.onChangeDataSource(ds!);
this.onQueriesChange([
{
refId: 'A',
datasource: {
type: 'grafana',
uid: 'grafana',
},
queryType: GrafanaQueryType.Snapshot,
snapshot: [],
},
]);
this.props.onRunQueries();
};
onFileDrop = (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => {
DFImport.filesToDataframes(acceptedFiles).subscribe(async (next) => {
const snapshot: DataFrameJSON[] = [];
next.dataFrames.forEach((df) => {
const dataframeJson = dataFrameToJSON(df);
snapshot.push(dataframeJson);
});
const ds = getDataSourceSrv().getInstanceSettings('-- Grafana --');
await this.onChangeDataSource(ds!);
this.onQueriesChange([
{
refId: 'A',
datasource: {
type: 'grafana',
uid: 'grafana',
},
queryType: GrafanaQueryType.Snapshot,
snapshot: snapshot,
file: next.file,
},
]);
this.props.onRunQueries();
});
};
onQueriesChange = (queries: DataQuery[] | GrafanaQuery[]) => {
this.onChange({ queries });
this.setState({ queries });

View File

@ -43,6 +43,16 @@ export const defaultQuery: GrafanaQuery = {
queryType: GrafanaQueryType.RandomWalk,
};
export const defaultFileUploadQuery: GrafanaQuery = {
refId: 'A',
datasource: {
type: 'grafana',
uid: 'grafana',
},
queryType: GrafanaQueryType.Snapshot,
snapshot: [],
};
//----------------------------------------------
// Annotations
//----------------------------------------------

View File

@ -1,10 +1,13 @@
import { DropEvent, FileRejection } from 'react-dropzone';
import { DataFrame, DataFrameJSON, dataFrameToJSON } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PanelModel } from 'app/features/dashboard/state';
import * as DFImport from 'app/features/dataframe-import';
import { ShowConfirmModalEvent } from 'app/types/events';
import { GrafanaQuery, GrafanaQueryType } from './types';
import { defaultFileUploadQuery, GrafanaQuery, GrafanaQueryType } from './types';
/**
* Will show a confirm modal if the current panel does not have a snapshot query.
@ -54,3 +57,18 @@ function updateSnapshotData(frames: DataFrame[], panel: PanelModel) {
panel.refresh();
}
export function getFileDropToQueryHandler(
onFileLoaded: (query: GrafanaQuery, fileRejections: FileRejection[]) => void
) {
return (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => {
DFImport.filesToDataframes(acceptedFiles).subscribe(async (next) => {
const snapshot: DataFrameJSON[] = [];
next.dataFrames.forEach((df: DataFrame) => {
const dataframeJson = dataFrameToJSON(df);
snapshot.push(dataframeJson);
});
onFileLoaded({ ...defaultFileUploadQuery, ...{ snapshot: snapshot, file: next.file } }, fileRejections);
});
};
}