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:
co-authored by
Ivan Ortega Alba
parent
27635e6f7b
commit
dc1600ff14
@@ -356,6 +356,7 @@ lerna.json @grafana/frontend-ops
|
|||||||
/public/app/features/datasources/ @grafana/user-essentials
|
/public/app/features/datasources/ @grafana/user-essentials
|
||||||
/public/app/features/dimensions/ @grafana/dataviz-squad
|
/public/app/features/dimensions/ @grafana/dataviz-squad
|
||||||
/public/app/features/dataframe-import/ @grafana/grafana-bi-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/explore/ @grafana/explore-squad
|
||||||
/public/app/features/expressions/ @grafana/observability-metrics
|
/public/app/features/expressions/ @grafana/observability-metrics
|
||||||
/public/app/features/folders/ @grafana/user-essentials
|
/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 |
|
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view |
|
||||||
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
|
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
|
||||||
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
||||||
|
| `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
|||||||
@@ -82,4 +82,5 @@ export interface FeatureToggles {
|
|||||||
logsContextDatasourceUi?: boolean;
|
logsContextDatasourceUi?: boolean;
|
||||||
lokiQuerySplitting?: boolean;
|
lokiQuerySplitting?: boolean;
|
||||||
individualCookiePreferences?: boolean;
|
individualCookiePreferences?: boolean;
|
||||||
|
drawerDataSourcePicker?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,5 +372,11 @@ var (
|
|||||||
Description: "Support overriding cookie preferences per user",
|
Description: "Support overriding cookie preferences per user",
|
||||||
State: FeatureStateAlpha,
|
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
|
// FlagIndividualCookiePreferences
|
||||||
// Support overriding cookie preferences per user
|
// Support overriding cookie preferences per user
|
||||||
FlagIndividualCookiePreferences = "individualCookiePreferences"
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 { 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,
|
DataQuery,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
@@ -18,8 +21,11 @@ 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 { DataSourcePickerWithHistory } from 'app/features/datasource-drawer/DataSourcePickerWithHistory';
|
||||||
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 { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
|
import { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
|
||||||
|
|
||||||
import { isQueryWithMixedDatasource } from '../../query-library/api/SavedQueriesApi';
|
import { isQueryWithMixedDatasource } from '../../query-library/api/SavedQueriesApi';
|
||||||
@@ -274,14 +280,32 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
Data source
|
Data source
|
||||||
</InlineFormLabel>
|
</InlineFormLabel>
|
||||||
<div className={styles.dataSourceRowItem}>
|
<div className={styles.dataSourceRowItem}>
|
||||||
<DataSourcePicker
|
{config.featureToggles.drawerDataSourcePicker ? (
|
||||||
onChange={this.onChangeDataSource}
|
<DataSourcePickerWithHistory
|
||||||
current={options.dataSource}
|
onChange={this.onChangeDataSource}
|
||||||
metrics={true}
|
current={options.dataSource}
|
||||||
mixed={true}
|
metrics={true}
|
||||||
dashboard={true}
|
mixed={true}
|
||||||
variables={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>
|
</div>
|
||||||
{dataSource && (
|
{dataSource && (
|
||||||
<>
|
<>
|
||||||
@@ -369,7 +393,32 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
this.onScrollBottom();
|
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.onChange({ queries });
|
||||||
this.setState({ queries });
|
this.setState({ queries });
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user