mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataSourcePicker: Add recently used from local storage to ds picker (#66985)
* Add recently used from local storage to ds picker
This commit is contained in:
parent
1cad819670
commit
d419402a43
@ -10,11 +10,13 @@ import { 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 { useDatasource } from '../../hooks';
|
||||||
|
|
||||||
import { DataSourceList } from './DataSourceList';
|
import { DataSourceList } from './DataSourceList';
|
||||||
import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo';
|
import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo';
|
||||||
import { DataSourceModal } from './DataSourceModal';
|
import { DataSourceModal } from './DataSourceModal';
|
||||||
import { PickerContentProps, DataSourceDropdownProps } from './types';
|
import { PickerContentProps, DataSourceDropdownProps } from './types';
|
||||||
import { dataSourceLabel, useGetDatasource } from './utils';
|
import { dataSourceLabel } from './utils';
|
||||||
|
|
||||||
export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
||||||
const { current, onChange, ...restProps } = props;
|
const { current, onChange, ...restProps } = props;
|
||||||
@ -24,7 +26,7 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
|||||||
const [selectorElement, setSelectorElement] = useState<HTMLDivElement | null>();
|
const [selectorElement, setSelectorElement] = useState<HTMLDivElement | null>();
|
||||||
const [filterTerm, setFilterTerm] = useState<string>();
|
const [filterTerm, setFilterTerm] = useState<string>();
|
||||||
|
|
||||||
const currentDataSourceInstanceSettings = useGetDatasource(current);
|
const currentDataSourceInstanceSettings = useDatasource(current);
|
||||||
|
|
||||||
const popper = usePopper(markerElement, selectorElement, {
|
const popper = usePopper(markerElement, selectorElement, {
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
||||||
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { useDatasources, useRecentlyUsedDataSources } from '../../hooks';
|
||||||
|
|
||||||
import { DataSourceCard } from './DataSourceCard';
|
import { DataSourceCard } from './DataSourceCard';
|
||||||
import { isDataSourceMatch, useGetDatasources } from './utils';
|
import { getDataSourceCompareFn, isDataSourceMatch } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component props description for the {@link DataSourceList}
|
* Component props description for the {@link DataSourceList}
|
||||||
@ -35,7 +38,7 @@ export interface DataSourceListProps {
|
|||||||
export function DataSourceList(props: DataSourceListProps) {
|
export function DataSourceList(props: DataSourceListProps) {
|
||||||
const { className, current, onChange } = props;
|
const { className, current, onChange } = props;
|
||||||
// QUESTION: Should we use data from the Redux store as admin DS view does?
|
// QUESTION: Should we use data from the Redux store as admin DS view does?
|
||||||
const dataSources = useGetDatasources({
|
const dataSources = useDatasources({
|
||||||
alerting: props.alerting,
|
alerting: props.alerting,
|
||||||
annotations: props.annotations,
|
annotations: props.annotations,
|
||||||
dashboard: props.dashboard,
|
dashboard: props.dashboard,
|
||||||
@ -48,18 +51,33 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
variables: props.variables,
|
variables: props.variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [recentlyUsedDataSources, pushRecentlyUsedDataSource] = useRecentlyUsedDataSources();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{dataSources
|
{dataSources
|
||||||
.filter((ds) => (props.filter ? props.filter(ds) : true))
|
.filter((ds) => (props.filter ? props.filter(ds) : true))
|
||||||
|
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
||||||
.map((ds) => (
|
.map((ds) => (
|
||||||
<DataSourceCard
|
<DataSourceCard
|
||||||
key={ds.uid}
|
key={ds.uid}
|
||||||
ds={ds}
|
ds={ds}
|
||||||
onClick={() => onChange(ds)}
|
onClick={() => {
|
||||||
|
pushRecentlyUsedDataSource(ds);
|
||||||
|
onChange(ds);
|
||||||
|
}}
|
||||||
selected={!!isDataSourceMatch(ds, current)}
|
selected={!!isDataSourceMatch(ds, current)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDataSourceVariableIDs() {
|
||||||
|
const templateSrv = getTemplateSrv();
|
||||||
|
/** Unforunately there is no easy way to identify data sources that are variables. The uid of the data source will be the name of the variable in a templating syntax $([name]) **/
|
||||||
|
return templateSrv
|
||||||
|
.getVariables()
|
||||||
|
.filter((v) => v.type === 'datasource')
|
||||||
|
.map((v) => `\${${v.id}}`);
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
||||||
|
|
||||||
import { isDataSourceMatch } from './utils';
|
import { isDataSourceMatch, getDataSourceCompareFn } from './utils';
|
||||||
|
|
||||||
describe('isDataSourceMatch', () => {
|
describe('isDataSourceMatch', () => {
|
||||||
const dataSourceInstanceSettings = { uid: 'a' } as DataSourceInstanceSettings;
|
const dataSourceInstanceSettings = { uid: 'a' } as DataSourceInstanceSettings;
|
||||||
@ -28,3 +28,68 @@ describe('isDataSourceMatch', () => {
|
|||||||
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceRef)).toBeFalsy();
|
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceRef)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDataSouceCompareFn', () => {
|
||||||
|
const dataSources = [
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
] as DataSourceInstanceSettings[];
|
||||||
|
|
||||||
|
it('sorts built in datasources last and other data sources alphabetically', () => {
|
||||||
|
dataSources.sort(getDataSourceCompareFn(undefined, [], []));
|
||||||
|
expect(dataSources).toEqual([
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
] as DataSourceInstanceSettings[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts the current datasource before others', () => {
|
||||||
|
dataSources.sort(getDataSourceCompareFn('c', [], []));
|
||||||
|
expect(dataSources).toEqual([
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
] as DataSourceInstanceSettings[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts recently used datasources first', () => {
|
||||||
|
dataSources.sort(getDataSourceCompareFn(undefined, ['c', 'a'], []));
|
||||||
|
expect(dataSources).toEqual([
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
] as DataSourceInstanceSettings[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts variables before other datasources', () => {
|
||||||
|
dataSources.sort(getDataSourceCompareFn(undefined, [], ['c', 'b']));
|
||||||
|
expect(dataSources).toEqual([
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
] as DataSourceInstanceSettings[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts datasources current -> recently used -> variables -> others -> built in', () => {
|
||||||
|
const dataSources = [
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'e', name: 'e', meta: { builtIn: false } },
|
||||||
|
{ uid: 'd', name: 'd', meta: { builtIn: false } },
|
||||||
|
{ uid: 'f', name: 'f', meta: { builtIn: false } },
|
||||||
|
] as DataSourceInstanceSettings[];
|
||||||
|
|
||||||
|
dataSources.sort(getDataSourceCompareFn('c', ['b', 'e'], ['d']));
|
||||||
|
expect(dataSources).toEqual([
|
||||||
|
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||||
|
{ uid: 'e', name: 'e', meta: { builtIn: false } },
|
||||||
|
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||||
|
{ uid: 'd', name: 'd', meta: { builtIn: false } },
|
||||||
|
{ uid: 'f', name: 'f', meta: { builtIn: false } },
|
||||||
|
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||||
|
] as DataSourceInstanceSettings[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data';
|
||||||
import { GetDataSourceListFilters, getDataSourceSrv } from '@grafana/runtime';
|
|
||||||
|
|
||||||
export function isDataSourceMatch(
|
export function isDataSourceMatch(
|
||||||
ds: DataSourceInstanceSettings | undefined,
|
ds: DataSourceInstanceSettings | undefined,
|
||||||
@ -39,22 +38,52 @@ export function dataSourceLabel(
|
|||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetDatasources(filters: GetDataSourceListFilters) {
|
export function getDataSourceCompareFn(
|
||||||
const dataSourceSrv = getDataSourceSrv();
|
current: DataSourceRef | DataSourceInstanceSettings | string | null | undefined,
|
||||||
|
recentlyUsedDataSources: string[],
|
||||||
return dataSourceSrv.getList(filters);
|
dataSourceVariablesIDs: string[]
|
||||||
|
) {
|
||||||
|
const cmpDataSources = (a: DataSourceInstanceSettings, b: DataSourceInstanceSettings) => {
|
||||||
|
// Sort the current ds before everything else.
|
||||||
|
if (current && isDataSourceMatch(a, current)) {
|
||||||
|
return -1;
|
||||||
|
} else if (current && isDataSourceMatch(b, current)) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetDatasource(dataSource: string | DataSourceRef | DataSourceInstanceSettings | null | undefined) {
|
// Sort recently used data sources by latest used, but after current.
|
||||||
const dataSourceSrv = getDataSourceSrv();
|
const aIndex = recentlyUsedDataSources.indexOf(a.uid);
|
||||||
|
const bIndex = recentlyUsedDataSources.indexOf(b.uid);
|
||||||
if (!dataSource) {
|
if (aIndex > -1 && aIndex > bIndex) {
|
||||||
return undefined;
|
return -1;
|
||||||
|
}
|
||||||
|
if (bIndex > -1 && bIndex > aIndex) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof dataSource === 'string') {
|
// Sort variables before the rest. Variables sorted alphabetically by name.
|
||||||
return dataSourceSrv.getInstanceSettings(dataSource);
|
const aIsVariable = dataSourceVariablesIDs.includes(a.uid);
|
||||||
|
const bIsVariable = dataSourceVariablesIDs.includes(b.uid);
|
||||||
|
if (aIsVariable && !bIsVariable) {
|
||||||
|
return -1;
|
||||||
|
} else if (bIsVariable && !aIsVariable) {
|
||||||
|
return 1;
|
||||||
|
} else if (bIsVariable && aIsVariable) {
|
||||||
|
return a.name < b.name ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSourceSrv.getInstanceSettings(dataSource);
|
// Sort built in DataSources to the bottom and alphabetically by name.
|
||||||
|
if (a.meta.builtIn && !b.meta.builtIn) {
|
||||||
|
return 1;
|
||||||
|
} else if (b.meta.builtIn && !a.meta.builtIn) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.meta.builtIn && b.meta.builtIn) {
|
||||||
|
return a.name < b.name ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the rest alphabetically by name.
|
||||||
|
return a.name < b.name ? -1 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmpDataSources;
|
||||||
}
|
}
|
||||||
|
57
public/app/features/datasources/hooks.ts
Normal file
57
public/app/features/datasources/hooks.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useLocalStorage } from 'react-use';
|
||||||
|
|
||||||
|
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
||||||
|
import { GetDataSourceListFilters, getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export const LOCAL_STORAGE_KEY = 'grafana.features.datasources.components.picker.DataSourceDropDown.history';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the uid of the last 5 data sources selected by the user. The last UID is the one most recently used.
|
||||||
|
*/
|
||||||
|
export function useRecentlyUsedDataSources(): [string[], (ds: DataSourceInstanceSettings) => void] {
|
||||||
|
const [value = [], setStorage] = useLocalStorage<string[]>(LOCAL_STORAGE_KEY, []);
|
||||||
|
|
||||||
|
const pushRecentlyUsedDataSource = useCallback(
|
||||||
|
(ds: DataSourceInstanceSettings) => {
|
||||||
|
if (ds.meta.builtIn) {
|
||||||
|
// Prevent storing the built in datasources (-- Grafana --, -- Mixed --, -- Dashboard --)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.includes(ds.uid)) {
|
||||||
|
// Prevent storing multiple copies of the same data source, put it at the front of the array instead.
|
||||||
|
value.splice(
|
||||||
|
value.findIndex((dsUid) => ds.uid === dsUid),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
setStorage([...value, ds.uid]);
|
||||||
|
} else {
|
||||||
|
setStorage([...value, ds.uid].slice(1, 6));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[value, setStorage]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, pushRecentlyUsedDataSource];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDatasources(filters: GetDataSourceListFilters) {
|
||||||
|
const dataSourceSrv = getDataSourceSrv();
|
||||||
|
const dataSources = dataSourceSrv.getList(filters);
|
||||||
|
|
||||||
|
return dataSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDatasource(dataSource: string | DataSourceRef | DataSourceInstanceSettings | null | undefined) {
|
||||||
|
const dataSourceSrv = getDataSourceSrv();
|
||||||
|
|
||||||
|
if (!dataSource) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dataSource === 'string') {
|
||||||
|
return dataSourceSrv.getInstanceSettings(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSourceSrv.getInstanceSettings(dataSource);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user