mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
DataSourcePicker: Add new style of data source picker. (#63736)
* MVP of a new datasource picker * Add datasource select history, naming DatasourceSelect -> DataSourceDrawer * refactor cards * Cleanup and fixing sort order for recents * add feature flag * fix feature flag name and use it * Highlight selected * Move new ds picker to core * Restore original datasource picker * Remove unused property * update yarn.lock * Rename folder, update codeowners * add test for util functions * Remove es-lint exception * Change feature toggle description * remove unnecessary if Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Make test a bit more clear Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Clean up api, filter once and before maps, minor code cleanup * Fix prettier issue --------- Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
parent
27635e6f7b
commit
dc1600ff14
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -356,6 +356,7 @@ lerna.json @grafana/frontend-ops
|
||||
/public/app/features/datasources/ @grafana/user-essentials
|
||||
/public/app/features/dimensions/ @grafana/dataviz-squad
|
||||
/public/app/features/dataframe-import/ @grafana/grafana-bi-squad
|
||||
/public/app/features/datasource-drawer/ @grafana/grafana-bi-squad
|
||||
/public/app/features/explore/ @grafana/explore-squad
|
||||
/public/app/features/expressions/ @grafana/observability-metrics
|
||||
/public/app/features/folders/ @grafana/user-essentials
|
||||
|
@ -92,6 +92,7 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view |
|
||||
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
|
||||
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
||||
| `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
@ -82,4 +82,5 @@ export interface FeatureToggles {
|
||||
logsContextDatasourceUi?: boolean;
|
||||
lokiQuerySplitting?: boolean;
|
||||
individualCookiePreferences?: boolean;
|
||||
drawerDataSourcePicker?: boolean;
|
||||
}
|
||||
|
@ -372,5 +372,11 @@ var (
|
||||
Description: "Support overriding cookie preferences per user",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "drawerDataSourcePicker",
|
||||
Description: "Changes the user experience for data source selection to a drawer.",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -270,4 +270,8 @@ const (
|
||||
// FlagIndividualCookiePreferences
|
||||
// Support overriding cookie preferences per user
|
||||
FlagIndividualCookiePreferences = "individualCookiePreferences"
|
||||
|
||||
// FlagDrawerDataSourcePicker
|
||||
// Changes the user experience for data source selection to a drawer.
|
||||
FlagDrawerDataSourcePicker = "drawerDataSourcePicker"
|
||||
)
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { isDataSourceMatch } from './DataSourceDrawer';
|
||||
|
||||
describe('DataSourceDrawer', () => {
|
||||
describe('isDataSourceMatch', () => {
|
||||
const dataSourceInstanceSettings = { uid: 'a' } as DataSourceInstanceSettings<DataSourceJsonData>;
|
||||
|
||||
it('matches a string with the uid', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, 'a')).toBeTruthy();
|
||||
});
|
||||
it('matches a datasource with a datasource by the uid', () => {
|
||||
expect(
|
||||
isDataSourceMatch(dataSourceInstanceSettings, { uid: 'a' } as DataSourceInstanceSettings<DataSourceJsonData>)
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('matches a datasource ref with a datasource by the uid', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'a' } as DataSourceRef)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('doesnt match with null', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, null)).toBeFalsy();
|
||||
});
|
||||
it('doesnt match a datasource to a non matching string', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, 'b')).toBeFalsy();
|
||||
});
|
||||
it('doesnt match a datasource with a different datasource uid', () => {
|
||||
expect(
|
||||
isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceInstanceSettings<DataSourceJsonData>)
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('doesnt match a datasource with a datasource ref with a different uid', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceRef)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
161
public/app/features/datasource-drawer/DataSourceDrawer.tsx
Normal file
161
public/app/features/datasource-drawer/DataSourceDrawer.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
CustomScrollbar,
|
||||
Drawer,
|
||||
FileDropzone,
|
||||
FileDropzoneDefaultChildren,
|
||||
Input,
|
||||
ModalsController,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { DataSourceCard } from './components/DataSourceCard';
|
||||
import { DataSourceDisplay } from './components/DataSourceDisplay';
|
||||
import { PickerContentProps, DataSourceDrawerProps } from './types';
|
||||
|
||||
export function DataSourceDrawer(props: DataSourceDrawerProps) {
|
||||
const { current, onChange } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<Button
|
||||
className={styles.picker}
|
||||
onClick={() => {
|
||||
showModal(PickerContent, {
|
||||
...props,
|
||||
onDismiss: hideModal,
|
||||
onChange: (ds) => {
|
||||
onChange(ds);
|
||||
hideModal();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DataSourceDisplay dataSource={current}></DataSourceDisplay>
|
||||
</Button>
|
||||
)}
|
||||
</ModalsController>
|
||||
);
|
||||
}
|
||||
|
||||
function PickerContent(props: PickerContentProps) {
|
||||
const { datasources, enableFileUpload, recentlyUsed = [], onChange, fileUploadOptions, onDismiss, current } = props;
|
||||
const changeCallback = useCallback(
|
||||
(ds: string) => {
|
||||
onChange(ds);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const [filterTerm, onFilterChange] = useState<string>('');
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const filteredDataSources = datasources.filter((ds) => {
|
||||
return ds?.name.toLocaleLowerCase().indexOf(filterTerm.toLocaleLowerCase()) !== -1;
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer closeOnMaskClick={true} onClose={onDismiss}>
|
||||
<div className={styles.drawerContent}>
|
||||
<div className={styles.filterContainer}>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
onFilterChange(e.currentTarget.value);
|
||||
}}
|
||||
value={filterTerm}
|
||||
></Input>
|
||||
</div>
|
||||
<div className={styles.dataSourceList}>
|
||||
<CustomScrollbar>
|
||||
{recentlyUsed
|
||||
.map((uid) => filteredDataSources.find((ds) => ds.uid === uid))
|
||||
.map((ds) => {
|
||||
if (!ds) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DataSourceCard
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
key={ds.uid}
|
||||
ds={ds}
|
||||
onChange={changeCallback}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{recentlyUsed && recentlyUsed.length > 0 && <hr />}
|
||||
{filteredDataSources.map((ds) => (
|
||||
<DataSourceCard
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
key={ds.uid}
|
||||
ds={ds}
|
||||
onChange={changeCallback}
|
||||
/>
|
||||
))}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
{enableFileUpload && (
|
||||
<div className={styles.additionalContent}>
|
||||
<FileDropzone
|
||||
readAs="readAsArrayBuffer"
|
||||
fileListRenderer={() => undefined}
|
||||
options={{
|
||||
...fileUploadOptions,
|
||||
onDrop: (...args) => {
|
||||
onDismiss();
|
||||
fileUploadOptions?.onDrop?.(...args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FileDropzoneDefaultChildren primaryText={'Upload file'} />
|
||||
</FileDropzone>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
drawerContent: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`,
|
||||
picker: css`
|
||||
background: ${theme.colors.background.secondary};
|
||||
`,
|
||||
filterContainer: css`
|
||||
padding-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
dataSourceList: css`
|
||||
height: 50px;
|
||||
flex-grow: 1;
|
||||
`,
|
||||
additionalContent: css`
|
||||
padding-top: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
export function isDataSourceMatch(
|
||||
ds: DataSourceInstanceSettings<DataSourceJsonData> | undefined,
|
||||
current: string | DataSourceInstanceSettings<DataSourceJsonData> | DataSourceRef | null | undefined
|
||||
): boolean | undefined {
|
||||
if (!ds) {
|
||||
return false;
|
||||
}
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
if (typeof current === 'string') {
|
||||
return ds.uid === current;
|
||||
}
|
||||
return ds.uid === current.uid;
|
||||
}
|
98
public/app/features/datasource-drawer/DataSourcePicker.tsx
Normal file
98
public/app/features/datasource-drawer/DataSourcePicker.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceRef, getDataSourceUID } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataSourceJsonData } from '@grafana/schema';
|
||||
|
||||
import { DataSourceDrawer } from './DataSourceDrawer';
|
||||
import { DataSourcePickerProps } from './types';
|
||||
|
||||
/**
|
||||
* Component state description for the {@link DataSourcePicker}
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface DataSourcePickerState {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to be able to select a datasource from the list of installed and enabled
|
||||
* datasources in the current Grafana instance.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataSourcePickerState> {
|
||||
dataSourceSrv = getDataSourceSrv();
|
||||
|
||||
state: DataSourcePickerState = {};
|
||||
|
||||
componentDidMount() {
|
||||
const { current } = this.props;
|
||||
const dsSettings = this.dataSourceSrv.getInstanceSettings(current);
|
||||
if (!dsSettings) {
|
||||
this.setState({ error: 'Could not find data source ' + current });
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (ds?: string) => {
|
||||
const dsSettings = this.dataSourceSrv.getInstanceSettings(ds);
|
||||
|
||||
if (dsSettings) {
|
||||
this.props.onChange(dsSettings);
|
||||
this.setState({ error: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
private getCurrentDs(): DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined {
|
||||
const { current, noDefault } = this.props;
|
||||
if (!current && noDefault) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ds = this.dataSourceSrv.getInstanceSettings(current);
|
||||
if (ds) {
|
||||
return ds;
|
||||
}
|
||||
|
||||
return getDataSourceUID(current);
|
||||
}
|
||||
|
||||
getDatasources() {
|
||||
const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } =
|
||||
this.props;
|
||||
|
||||
return this.dataSourceSrv.getList({
|
||||
alerting,
|
||||
tracing,
|
||||
metrics,
|
||||
logs,
|
||||
dashboard,
|
||||
mixed,
|
||||
variables,
|
||||
annotations,
|
||||
pluginId,
|
||||
filter,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { recentlyUsed, fileUploadOptions, enableFileUpload } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataSourceDrawer
|
||||
datasources={this.getDatasources()}
|
||||
onChange={this.onChange}
|
||||
recentlyUsed={recentlyUsed}
|
||||
current={this.getCurrentDs()}
|
||||
fileUploadOptions={fileUploadOptions}
|
||||
enableFileUpload={enableFileUpload}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { updateHistory } from './DataSourcePickerWithHistory';
|
||||
|
||||
describe('DataSourcePickerWithHistory', () => {
|
||||
describe('updateHistory', () => {
|
||||
const early = { uid: 'b', lastUse: '2023-02-27T13:39:08.318Z' };
|
||||
const later = { uid: 'a', lastUse: '2023-02-28T13:39:08.318Z' };
|
||||
|
||||
it('should add an item to the history', () => {
|
||||
expect(updateHistory([], early)).toEqual([early]);
|
||||
});
|
||||
|
||||
it('should sort later entries first', () => {
|
||||
expect(updateHistory([early], later)).toEqual([later, early]);
|
||||
});
|
||||
|
||||
it('should update an already existing history item with the new lastUsed date', () => {
|
||||
const laterB = { uid: early.uid, lastUse: later.lastUse };
|
||||
expect(updateHistory([early], laterB)).toEqual([laterB]);
|
||||
});
|
||||
|
||||
it('should keep the three latest items in history', () => {
|
||||
const evenLater = { uid: 'c', lastUse: '2023-03-01T13:39:08.318Z' };
|
||||
const latest = { uid: 'd', lastUse: '2023-03-02T13:39:08.318Z' };
|
||||
expect(updateHistory([early, later, evenLater], latest)).toEqual([latest, evenLater, later]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
||||
|
||||
import { DataSourcePicker } from './DataSourcePicker';
|
||||
import { DataSourcePickerHistoryItem, DataSourcePickerWithHistoryProps } from './types';
|
||||
|
||||
const DS_PICKER_STORAGE_KEY = 'DATASOURCE_PICKER';
|
||||
|
||||
export const DataSourcePickerWithHistory = (props: DataSourcePickerWithHistoryProps) => {
|
||||
return (
|
||||
<LocalStorageValueProvider<DataSourcePickerHistoryItem[]>
|
||||
defaultValue={[]}
|
||||
storageKey={props.localStorageKey ?? DS_PICKER_STORAGE_KEY}
|
||||
>
|
||||
{(rawValues, onSaveToStore) => {
|
||||
return (
|
||||
<DataSourcePicker
|
||||
{...props}
|
||||
recentlyUsed={rawValues.map((dsi) => dsi.uid)} //Filter recently to have a time cutoff
|
||||
onChange={(ds) => {
|
||||
onSaveToStore(updateHistory(rawValues, { uid: ds.uid, lastUse: dateTime(new Date()).toISOString() }));
|
||||
props.onChange(ds);
|
||||
}}
|
||||
></DataSourcePicker>
|
||||
);
|
||||
}}
|
||||
</LocalStorageValueProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export function updateHistory(values: DataSourcePickerHistoryItem[], newValue: DataSourcePickerHistoryItem) {
|
||||
const newHistory = values;
|
||||
const existingIndex = newHistory.findIndex((dpi) => dpi.uid === newValue.uid);
|
||||
if (existingIndex !== -1) {
|
||||
newHistory[existingIndex] = newValue;
|
||||
} else {
|
||||
newHistory.push(newValue);
|
||||
}
|
||||
|
||||
newHistory.sort((a, b) => {
|
||||
const al = dateTime(a.lastUse);
|
||||
const bl = dateTime(b.lastUse);
|
||||
if (al.isBefore(bl)) {
|
||||
return 1;
|
||||
} else if (bl.isBefore(al)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return newHistory.slice(0, 3);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, PluginSignatureBadge, Tag, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface DataSourceCardProps {
|
||||
onChange: (uid: string) => void;
|
||||
selected?: boolean;
|
||||
ds: DataSourceInstanceSettings<DataSourceJsonData>;
|
||||
}
|
||||
|
||||
export function DataSourceCard(props: DataSourceCardProps) {
|
||||
const { selected, ds, onChange } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<Card className={selected ? styles.selectedDataSource : undefined} key={ds.uid} onClick={() => onChange(ds.uid)}>
|
||||
<Card.Figure>
|
||||
<img alt={`${ds.meta.name} logo`} src={ds.meta.info.logos.large}></img>
|
||||
</Card.Figure>
|
||||
<Card.Meta>
|
||||
{[ds.meta.name, ds.url, ds.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />]}
|
||||
</Card.Meta>
|
||||
<Card.Tags>
|
||||
<PluginSignatureBadge status={ds.meta.signature} />
|
||||
</Card.Tags>
|
||||
<Card.Heading>{ds.name}</Card.Heading>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
selectedDataSource: css`
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.1)};
|
||||
`,
|
||||
};
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface DataSourceDisplayProps {
|
||||
dataSource: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
|
||||
}
|
||||
|
||||
export function DataSourceDisplay(props: DataSourceDisplayProps) {
|
||||
const { dataSource } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!dataSource) {
|
||||
return <span>Unknown</span>;
|
||||
}
|
||||
|
||||
if (typeof dataSource === 'string') {
|
||||
return <span>${dataSource} - not found</span>;
|
||||
}
|
||||
|
||||
if ('name' in dataSource) {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
className={styles.pickerDSLogo}
|
||||
alt={`${dataSource.meta.name} logo`}
|
||||
src={dataSource.meta.info.logos.small}
|
||||
></img>
|
||||
<span>{dataSource.name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{dataSource.uid} - not found</span>;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
pickerDSLogo: css`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
}
|
52
public/app/features/datasource-drawer/types.ts
Normal file
52
public/app/features/datasource-drawer/types.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { DropzoneOptions } from 'react-dropzone';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
|
||||
|
||||
export interface DataSourceDrawerProps {
|
||||
datasources: Array<DataSourceInstanceSettings<DataSourceJsonData>>;
|
||||
onFileDrop?: () => void;
|
||||
onChange: (ds: string) => void;
|
||||
current: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
|
||||
enableFileUpload?: boolean;
|
||||
fileUploadOptions?: DropzoneOptions;
|
||||
recentlyUsed?: string[];
|
||||
}
|
||||
|
||||
export interface PickerContentProps extends DataSourceDrawerProps {
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export interface DataSourcePickerProps {
|
||||
onChange: (ds: DataSourceInstanceSettings) => void;
|
||||
current: DataSourceRef | string | null; // uid
|
||||
tracing?: boolean;
|
||||
recentlyUsed?: string[];
|
||||
mixed?: boolean;
|
||||
dashboard?: boolean;
|
||||
metrics?: boolean;
|
||||
type?: string | string[];
|
||||
annotations?: boolean;
|
||||
variables?: boolean;
|
||||
alerting?: boolean;
|
||||
pluginId?: string;
|
||||
/** If true,we show only DSs with logs; and if true, pluginId shouldnt be passed in */
|
||||
logs?: boolean;
|
||||
// Does not set the default data source if there is no value.
|
||||
noDefault?: boolean;
|
||||
inputId?: string;
|
||||
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
|
||||
onClear?: () => void;
|
||||
disabled?: boolean;
|
||||
enableFileUpload?: boolean;
|
||||
fileUploadOptions?: DropzoneOptions;
|
||||
}
|
||||
|
||||
export interface DataSourcePickerWithHistoryProps extends Omit<DataSourcePickerProps, 'recentlyUsed'> {
|
||||
localStorageKey?: string;
|
||||
}
|
||||
|
||||
export interface DataSourcePickerHistoryItem {
|
||||
lastUse: string;
|
||||
uid: string;
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
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,
|
||||
@ -18,8 +21,11 @@ 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 { DataSourcePickerWithHistory } from 'app/features/datasource-drawer/DataSourcePickerWithHistory';
|
||||
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 { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { isQueryWithMixedDatasource } from '../../query-library/api/SavedQueriesApi';
|
||||
@ -274,14 +280,32 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
Data source
|
||||
</InlineFormLabel>
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<DataSourcePicker
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
/>
|
||||
{config.featureToggles.drawerDataSourcePicker ? (
|
||||
<DataSourcePickerWithHistory
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
enableFileUpload={config.featureToggles.editPanelCSVDragAndDrop}
|
||||
fileUploadOptions={{
|
||||
onDrop: this.onFileDrop,
|
||||
maxSize: DFImport.maxFileSize,
|
||||
multiple: false,
|
||||
accept: DFImport.acceptedFiles,
|
||||
}}
|
||||
></DataSourcePickerWithHistory>
|
||||
) : (
|
||||
<DataSourcePicker
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
></DataSourcePicker>
|
||||
)}
|
||||
</div>
|
||||
{dataSource && (
|
||||
<>
|
||||
@ -369,7 +393,32 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
this.onScrollBottom();
|
||||
};
|
||||
|
||||
onQueriesChange = (queries: DataQuery[]) => {
|
||||
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 });
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user