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 });
|
const grafanaDataSources = useDatasources({ mixed, dashboard, filter: (ds) => !!ds.meta.builtIn });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className} data-testid="built-in-data-sources-list">
|
||||||
{grafanaDataSources.map((ds) => {
|
{grafanaDataSources.map((ds) => {
|
||||||
return (
|
return (
|
||||||
<DataSourceCard
|
<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 { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { DataSourceJsonData } from '@grafana/schema';
|
import { DataQuery, DataSourceJsonData } from '@grafana/schema';
|
||||||
import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui';
|
import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
||||||
|
import { defaultFileUploadQuery, GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||||
|
|
||||||
import { useDatasource } from '../../hooks';
|
import { useDatasource } from '../../hooks';
|
||||||
|
|
||||||
@ -68,6 +69,15 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
|||||||
});
|
});
|
||||||
return () => sub.unsubscribe();
|
return () => sub.unsubscribe();
|
||||||
});
|
});
|
||||||
|
const grafanaDS = useDatasource('-- Grafana --');
|
||||||
|
|
||||||
|
const onClickAddCSV = () => {
|
||||||
|
if (!grafanaDS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(grafanaDS, [defaultFileUploadQuery]);
|
||||||
|
};
|
||||||
|
|
||||||
const currentDataSourceInstanceSettings = useDatasource(current);
|
const currentDataSourceInstanceSettings = useDatasource(current);
|
||||||
|
|
||||||
@ -154,14 +164,18 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
|||||||
<PickerContent
|
<PickerContent
|
||||||
keyboardEvents={keyboardEvents}
|
keyboardEvents={keyboardEvents}
|
||||||
filterTerm={filterTerm}
|
filterTerm={filterTerm}
|
||||||
onChange={(ds: DataSourceInstanceSettings<DataSourceJsonData>) => {
|
onChange={(
|
||||||
|
ds: DataSourceInstanceSettings<DataSourceJsonData>,
|
||||||
|
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||||
|
) => {
|
||||||
onClose();
|
onClose();
|
||||||
onChange(ds);
|
onChange(ds, defaultQueries);
|
||||||
}}
|
}}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
current={currentDataSourceInstanceSettings}
|
current={currentDataSourceInstanceSettings}
|
||||||
style={popper.styles.popper}
|
style={popper.styles.popper}
|
||||||
ref={setSelectorElement}
|
ref={setSelectorElement}
|
||||||
|
onClickAddCSV={onClickAddCSV}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
onDismiss={onClose}
|
onDismiss={onClose}
|
||||||
{...popper.attributes.popper}
|
{...popper.attributes.popper}
|
||||||
@ -237,15 +251,13 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
showModal(DataSourceModal, {
|
showModal(DataSourceModal, {
|
||||||
enableFileUpload: props.enableFileUpload,
|
|
||||||
fileUploadOptions: props.fileUploadOptions,
|
|
||||||
reportedInteractionFrom: 'ds_picker',
|
reportedInteractionFrom: 'ds_picker',
|
||||||
dashboard: props.dashboard,
|
dashboard: props.dashboard,
|
||||||
mixed: props.mixed,
|
mixed: props.mixed,
|
||||||
current,
|
current,
|
||||||
onDismiss: hideModal,
|
onDismiss: hideModal,
|
||||||
onChange: (ds) => {
|
onChange: (ds, defaultQueries) => {
|
||||||
onChange(ds);
|
onChange(ds, defaultQueries);
|
||||||
hideModal();
|
hideModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -71,7 +71,7 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
|
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
|
||||||
|
|
||||||
return (
|
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 && (
|
{filteredDataSources.length === 0 && (
|
||||||
<EmptyState className={styles.emptyState} onClickCTA={onClickEmptyStateCTA} />
|
<EmptyState className={styles.emptyState} onClickCTA={onClickEmptyStateCTA} />
|
||||||
)}
|
)}
|
||||||
@ -79,6 +79,7 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
||||||
.map((ds) => (
|
.map((ds) => (
|
||||||
<DataSourceCard
|
<DataSourceCard
|
||||||
|
data-testid="data-source-card"
|
||||||
key={ds.uid}
|
key={ds.uid}
|
||||||
ds={ds}
|
ds={ds}
|
||||||
onClick={() => {
|
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 { css } from '@emotion/css';
|
||||||
import { once } from 'lodash';
|
import { once } from 'lodash';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { DropzoneOptions } from 'react-dropzone';
|
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
@ -15,6 +15,10 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import * as DFImport from 'app/features/dataframe-import';
|
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 { AddNewDataSourceButton } from './AddNewDataSourceButton';
|
||||||
import { BuiltInDataSourceList } from './BuiltInDataSourceList';
|
import { BuiltInDataSourceList } from './BuiltInDataSourceList';
|
||||||
@ -32,22 +36,18 @@ const INTERACTION_ITEM = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface DataSourceModalProps {
|
interface DataSourceModalProps {
|
||||||
onChange: (ds: DataSourceInstanceSettings) => void;
|
onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
|
||||||
current: DataSourceRef | string | null | undefined;
|
current: DataSourceRef | string | null | undefined;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
recentlyUsed?: string[];
|
recentlyUsed?: string[];
|
||||||
enableFileUpload?: boolean;
|
|
||||||
dashboard?: boolean;
|
dashboard?: boolean;
|
||||||
mixed?: boolean;
|
mixed?: boolean;
|
||||||
fileUploadOptions?: DropzoneOptions;
|
|
||||||
reportedInteractionFrom?: string;
|
reportedInteractionFrom?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataSourceModal({
|
export function DataSourceModal({
|
||||||
enableFileUpload,
|
|
||||||
dashboard,
|
dashboard,
|
||||||
mixed,
|
mixed,
|
||||||
fileUploadOptions,
|
|
||||||
onChange,
|
onChange,
|
||||||
current,
|
current,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
@ -78,6 +78,24 @@ export function DataSourceModal({
|
|||||||
[analyticsInteractionSrc]
|
[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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Select data source"
|
title="Select data source"
|
||||||
@ -91,6 +109,7 @@ export function DataSourceModal({
|
|||||||
>
|
>
|
||||||
<div className={styles.leftColumn}>
|
<div className={styles.leftColumn}>
|
||||||
<Input
|
<Input
|
||||||
|
type="search"
|
||||||
autoFocus
|
autoFocus
|
||||||
className={styles.searchInput}
|
className={styles.searchInput}
|
||||||
value={search}
|
value={search}
|
||||||
@ -135,7 +154,7 @@ export function DataSourceModal({
|
|||||||
mixed={mixed}
|
mixed={mixed}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
{enableFileUpload && (
|
{config.featureToggles.editPanelCSVDragAndDrop && (
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
readAs="readAsArrayBuffer"
|
readAs="readAsArrayBuffer"
|
||||||
fileListRenderer={() => undefined}
|
fileListRenderer={() => undefined}
|
||||||
@ -143,15 +162,7 @@ export function DataSourceModal({
|
|||||||
maxSize: DFImport.maxFileSize,
|
maxSize: DFImport.maxFileSize,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
accept: DFImport.acceptedFiles,
|
accept: DFImport.acceptedFiles,
|
||||||
...fileUploadOptions,
|
onDrop: onFileDrop,
|
||||||
onDrop: (...args) => {
|
|
||||||
fileUploadOptions?.onDrop?.(...args);
|
|
||||||
onDismiss();
|
|
||||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
|
||||||
item: INTERACTION_ITEM.UPLOAD_FILE,
|
|
||||||
src: analyticsInteractionSrc,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FileDropzoneDefaultChildren />
|
<FileDropzoneDefaultChildren />
|
||||||
|
@ -1,24 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DropzoneOptions } from 'react-dropzone';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
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 {
|
export interface DataSourceDropdownProps {
|
||||||
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>) => void;
|
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
|
||||||
current: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
|
current: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
|
||||||
enableFileUpload?: boolean;
|
tracing?: boolean;
|
||||||
fileUploadOptions?: DropzoneOptions;
|
mixed?: boolean;
|
||||||
onClickAddCSV?: () => void;
|
dashboard?: boolean;
|
||||||
|
metrics?: boolean;
|
||||||
|
type?: string | string[];
|
||||||
|
annotations?: boolean;
|
||||||
|
variables?: boolean;
|
||||||
|
alerting?: boolean;
|
||||||
|
pluginId?: string;
|
||||||
|
logs?: boolean;
|
||||||
recentlyUsed?: string[];
|
recentlyUsed?: string[];
|
||||||
hideTextValue?: boolean;
|
hideTextValue?: boolean;
|
||||||
dashboard?: boolean;
|
|
||||||
mixed?: boolean;
|
|
||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PickerContentProps extends DataSourceDropdownProps {
|
export interface PickerContentProps extends DataSourceDropdownProps {
|
||||||
|
onClickAddCSV?: () => void;
|
||||||
keyboardEvents: Observable<React.KeyboardEvent>;
|
keyboardEvents: Observable<React.KeyboardEvent>;
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
filterTerm?: string;
|
filterTerm?: string;
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { DropEvent, FileRejection } from 'react-dropzone';
|
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoreApp,
|
CoreApp,
|
||||||
DataFrameJSON,
|
|
||||||
dataFrameToJSON,
|
|
||||||
DataQuery,
|
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
getDefaultTimeRange,
|
getDefaultTimeRange,
|
||||||
@ -16,17 +12,17 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getDataSourceSrv, locationService } from '@grafana/runtime';
|
import { getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui';
|
import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui';
|
||||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { addQuery, queryIsEmpty } from 'app/core/utils/query';
|
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 { DataSourceModal } from 'app/features/datasources/components/picker/DataSourceModal';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
|
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 { QueryGroupOptions } from 'app/types';
|
||||||
|
|
||||||
import { PanelQueryRunner } from '../state/PanelQueryRunner';
|
import { PanelQueryRunner } from '../state/PanelQueryRunner';
|
||||||
@ -136,13 +132,16 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => {
|
onChangeDataSource = async (
|
||||||
|
newSettings: DataSourceInstanceSettings,
|
||||||
|
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||||
|
) => {
|
||||||
const { dsSettings } = this.state;
|
const { dsSettings } = this.state;
|
||||||
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
|
const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined;
|
||||||
const nextDS = await getDataSourceSrv().get(newSettings.uid);
|
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
|
// 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);
|
const dataSource = await this.dataSourceSrv.get(newSettings.name);
|
||||||
|
|
||||||
@ -161,6 +160,10 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
dataSource: dataSource,
|
dataSource: dataSource,
|
||||||
dsSettings: newSettings,
|
dsSettings: newSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (defaultQueries) {
|
||||||
|
this.props.onRunQueries();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onAddQueryClick = () => {
|
onAddQueryClick = () => {
|
||||||
@ -269,16 +272,9 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
const { isDataSourceModalOpen } = this.state;
|
const { isDataSourceModalOpen } = this.state;
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
enableFileUpload: config.featureToggles.editPanelCSVDragAndDrop,
|
|
||||||
fileUploadOptions: {
|
|
||||||
onDrop: this.onFileDrop,
|
|
||||||
maxSize: DFImport.maxFileSize,
|
|
||||||
multiple: false,
|
|
||||||
accept: DFImport.acceptedFiles,
|
|
||||||
},
|
|
||||||
current: this.props.options.dataSource,
|
current: this.props.options.dataSource,
|
||||||
onChange: (ds: DataSourceInstanceSettings) => {
|
onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
|
||||||
this.onChangeDataSource(ds);
|
await this.onChangeDataSource(ds, defaultQueries);
|
||||||
this.onCloseDataSourceModal();
|
this.onCloseDataSourceModal();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -288,14 +284,7 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
|
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
|
||||||
<DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal>
|
<DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal>
|
||||||
)}
|
)}
|
||||||
<DataSourcePicker
|
<DataSourcePicker {...commonProps} metrics={true} mixed={true} dashboard={true} variables={true} />
|
||||||
{...commonProps}
|
|
||||||
metrics={true}
|
|
||||||
mixed={true}
|
|
||||||
dashboard={true}
|
|
||||||
variables={true}
|
|
||||||
onClickAddCSV={this.onClickAddCSV}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -306,49 +295,6 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
this.onScrollBottom();
|
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[]) => {
|
onQueriesChange = (queries: DataQuery[] | GrafanaQuery[]) => {
|
||||||
this.onChange({ queries });
|
this.onChange({ queries });
|
||||||
this.setState({ queries });
|
this.setState({ queries });
|
||||||
|
@ -43,6 +43,16 @@ export const defaultQuery: GrafanaQuery = {
|
|||||||
queryType: GrafanaQueryType.RandomWalk,
|
queryType: GrafanaQueryType.RandomWalk,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultFileUploadQuery: GrafanaQuery = {
|
||||||
|
refId: 'A',
|
||||||
|
datasource: {
|
||||||
|
type: 'grafana',
|
||||||
|
uid: 'grafana',
|
||||||
|
},
|
||||||
|
queryType: GrafanaQueryType.Snapshot,
|
||||||
|
snapshot: [],
|
||||||
|
};
|
||||||
|
|
||||||
//----------------------------------------------
|
//----------------------------------------------
|
||||||
// Annotations
|
// Annotations
|
||||||
//----------------------------------------------
|
//----------------------------------------------
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import { DropEvent, FileRejection } from 'react-dropzone';
|
||||||
|
|
||||||
import { DataFrame, DataFrameJSON, dataFrameToJSON } from '@grafana/data';
|
import { DataFrame, DataFrameJSON, dataFrameToJSON } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
|
import * as DFImport from 'app/features/dataframe-import';
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
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.
|
* 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();
|
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