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