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); + }); + }; +}