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:
Oscar Kilhed 2023-04-21 17:07:11 +02:00 committed by GitHub
parent 1cad819670
commit d419402a43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 24 deletions

View File

@ -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',

View File

@ -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}}`);
}

View File

@ -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[]);
});
});

View File

@ -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;
} }

View 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);
}