diff --git a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx
index de1d4e0b38c..e44279b2d5b 100644
--- a/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx
+++ b/public/app/features/datasources/components/picker/BuiltInDataSourceList.tsx
@@ -26,7 +26,7 @@ export function BuiltInDataSourceList({ className, current, onChange, dashboard,
const grafanaDataSources = useDatasources({ mixed, dashboard, filter: (ds) => !!ds.meta.builtIn });
return (
-
+
{grafanaDataSources.map((ds) => {
return (
{}, 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);
+ 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();
+
+ 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(
+
+
+
+
+ );
+
+ 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;
+}
diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
index 5293f370824..1faef407014 100644
--- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
@@ -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) {
) => {
+ onChange={(
+ ds: DataSourceInstanceSettings,
+ 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((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();
},
});
diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx
index a56a8daa1f3..a9cbb96f56e 100644
--- a/public/app/features/datasources/components/picker/DataSourceList.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceList.tsx
@@ -71,7 +71,7 @@ export function DataSourceList(props: DataSourceListProps) {
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
return (
-
+
{filteredDataSources.length === 0 && (
)}
@@ -79,6 +79,7 @@ export function DataSourceList(props: DataSourceListProps) {
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
.map((ds) => (
{
diff --git a/public/app/features/datasources/components/picker/DataSourceModal.test.tsx b/public/app/features/datasources/components/picker/DataSourceModal.test.tsx
new file mode 100644
index 00000000000..3e79a911457
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourceModal.test.tsx
@@ -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();
+};
+
+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);
+ });
+ });
+});
diff --git a/public/app/features/datasources/components/picker/DataSourceModal.tsx b/public/app/features/datasources/components/picker/DataSourceModal.tsx
index d48a3144a3d..3ec15b0b072 100644
--- a/public/app/features/datasources/components/picker/DataSourceModal.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceModal.tsx
@@ -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 (
- {enableFileUpload && (
+ {config.featureToggles.editPanelCSVDragAndDrop && (
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,
}}
>
diff --git a/public/app/features/datasources/components/picker/types.ts b/public/app/features/datasources/components/picker/types.ts
index 801338a9091..f83d6328c88 100644
--- a/public/app/features/datasources/components/picker/types.ts
+++ b/public/app/features/datasources/components/picker/types.ts
@@ -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) => void;
+ onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current: DataSourceInstanceSettings | 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;
style: React.CSSProperties;
filterTerm?: string;
diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx
index c4f89a86cfc..d83ec4eefdf 100644
--- a/public/app/features/query/components/QueryGroup.tsx
+++ b/public/app/features/query/components/QueryGroup.tsx
@@ -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 {
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 {
dataSource: dataSource,
dsSettings: newSettings,
});
+
+ if (defaultQueries) {
+ this.props.onRunQueries();
+ }
};
onAddQueryClick = () => {
@@ -269,16 +272,9 @@ export class QueryGroup extends PureComponent {
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 {
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
)}
-
+
>
);
};
@@ -306,49 +295,6 @@ export class QueryGroup extends PureComponent {
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 });
diff --git a/public/app/plugins/datasource/grafana/types.ts b/public/app/plugins/datasource/grafana/types.ts
index b26c07c1db8..a6680231dad 100644
--- a/public/app/plugins/datasource/grafana/types.ts
+++ b/public/app/plugins/datasource/grafana/types.ts
@@ -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
//----------------------------------------------
diff --git a/public/app/plugins/datasource/grafana/utils.ts b/public/app/plugins/datasource/grafana/utils.ts
index 1e7972f5888..7e4d0585beb 100644
--- a/public/app/plugins/datasource/grafana/utils.ts
+++ b/public/app/plugins/datasource/grafana/utils.ts
@@ -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);
+ });
+ };
+}