mirror of
https://github.com/grafana/grafana.git
synced 2025-09-19 23:14:25 +08:00
DatasourcePicker: Favorite datasources (#109404)
This commit is contained in:

committed by
GitHub

parent
842fd44e83
commit
2e4b10e81c
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -608,6 +608,7 @@
|
||||
/packages/grafana-runtime/src/utils/returnToPrevious.ts @grafana/grafana-search-navigate-organise
|
||||
/packages/grafana-runtime/src/utils/toDataQueryError.ts @grafana/grafana-datasources-core-services
|
||||
/packages/grafana-runtime/src/utils/userStorage* @grafana/plugins-platform-frontend @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/utils/useFavoriteDatasources* @grafana/plugins-platform-frontend
|
||||
|
||||
# @grafana/schema
|
||||
/packages/grafana-schema/ @grafana/grafana-app-platform-squad
|
||||
|
@ -57,6 +57,7 @@ export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermis
|
||||
export { QueryEditorWithMigration } from './components/QueryEditorWithMigration';
|
||||
export { type MigrationHandler, isMigrationHandler, migrateQuery, migrateRequest } from './utils/migrationHandler';
|
||||
export { usePluginUserStorage } from './utils/userStorage';
|
||||
export { useFavoriteDatasources } from './utils/useFavoriteDatasources';
|
||||
export { FolderPicker, setFolderPicker } from './components/FolderPicker';
|
||||
export {
|
||||
type CorrelationsService,
|
||||
|
@ -0,0 +1,271 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginType, PluginMetaInfo } from '@grafana/data';
|
||||
|
||||
import { useFavoriteDatasources } from './useFavoriteDatasources';
|
||||
|
||||
// Mock UserStorage
|
||||
const mockGetItem = jest.fn();
|
||||
const mockSetItem = jest.fn();
|
||||
|
||||
jest.mock('./userStorage', () => {
|
||||
return {
|
||||
UserStorage: jest.fn().mockImplementation(() => ({
|
||||
getItem: mockGetItem,
|
||||
setItem: mockSetItem,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useFavoriteDatasources', () => {
|
||||
// Test data helpers
|
||||
const pluginMetaInfo: PluginMetaInfo = {
|
||||
author: { name: '', url: '' },
|
||||
description: '',
|
||||
version: '',
|
||||
updated: '',
|
||||
links: [],
|
||||
logos: { small: '', large: '' },
|
||||
screenshots: [],
|
||||
};
|
||||
|
||||
function createPluginMeta(name: string, builtIn: boolean): DataSourcePluginMeta {
|
||||
return {
|
||||
builtIn,
|
||||
name,
|
||||
id: name,
|
||||
type: PluginType.datasource,
|
||||
baseUrl: '',
|
||||
info: pluginMetaInfo,
|
||||
module: '',
|
||||
};
|
||||
}
|
||||
|
||||
function createDataSource(name: string, id: number, builtIn = false): DataSourceInstanceSettings {
|
||||
return {
|
||||
name: name,
|
||||
uid: `${name}-uid`,
|
||||
meta: createPluginMeta(name, builtIn),
|
||||
id,
|
||||
access: 'direct',
|
||||
jsonData: {},
|
||||
type: name,
|
||||
readOnly: false,
|
||||
};
|
||||
}
|
||||
|
||||
const mockDS1 = createDataSource('prometheus', 1);
|
||||
const mockBuiltInDS = createDataSource('grafana', 4, true);
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetItem.mockReset();
|
||||
mockSetItem.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initial loading', () => {
|
||||
it('should initialize with empty favorites when no stored data exists', async () => {
|
||||
mockGetItem.mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
expect(result.current.initialFavoriteDataSources).toEqual([]);
|
||||
});
|
||||
|
||||
expect(mockGetItem).toHaveBeenCalledWith('favoriteDatasources');
|
||||
});
|
||||
|
||||
it('should load existing favorites from storage', async () => {
|
||||
const storedFavorites = ['prometheus-uid', 'loki-uid'];
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(storedFavorites));
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(storedFavorites);
|
||||
expect(result.current.initialFavoriteDataSources).toEqual(storedFavorites);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-array data in storage gracefully', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify({ not: 'an array' }));
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
expect(result.current.initialFavoriteDataSources).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFavoriteDatasource', () => {
|
||||
it('should add a new datasource to favorites', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify([]));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addFavoriteDatasource(mockDS1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid']);
|
||||
expect(result.current.initialFavoriteDataSources).toEqual([]);
|
||||
});
|
||||
|
||||
expect(mockSetItem).toHaveBeenCalledWith('favoriteDatasources', JSON.stringify(['prometheus-uid']));
|
||||
});
|
||||
|
||||
it('should not add duplicate datasources', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['prometheus-uid']));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid']);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addFavoriteDatasource(mockDS1);
|
||||
});
|
||||
|
||||
// Should not change
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid']);
|
||||
expect(mockSetItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add built-in datasources', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify([]));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.addFavoriteDatasource(mockBuiltInDS);
|
||||
});
|
||||
|
||||
// Should not change
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
expect(mockSetItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFavoriteDatasource', () => {
|
||||
it('should remove a datasource from favorites', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['prometheus-uid', 'loki-uid']));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid', 'loki-uid']);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.removeFavoriteDatasource(mockDS1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['loki-uid']);
|
||||
});
|
||||
|
||||
expect(mockSetItem).toHaveBeenCalledWith('favoriteDatasources', JSON.stringify(['loki-uid']));
|
||||
});
|
||||
|
||||
it('should handle removing non-existent datasource gracefully', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['loki-uid']));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['loki-uid']);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.removeFavoriteDatasource(mockDS1); // prometheus not in list
|
||||
});
|
||||
|
||||
// Should not change or call setItem since nothing was removed
|
||||
expect(result.current.favoriteDatasources).toEqual(['loki-uid']);
|
||||
expect(mockSetItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove all datasources when removing the last one', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['prometheus-uid']));
|
||||
mockSetItem.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid']);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.removeFavoriteDatasource(mockDS1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
});
|
||||
|
||||
expect(mockSetItem).toHaveBeenCalledWith('favoriteDatasources', JSON.stringify([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFavoriteDatasource', () => {
|
||||
it('should return true for favorited datasource', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['prometheus-uid', 'loki-uid']));
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid', 'loki-uid']);
|
||||
});
|
||||
|
||||
expect(result.current.isFavoriteDatasource('prometheus-uid')).toBe(true);
|
||||
expect(result.current.isFavoriteDatasource('loki-uid')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-favorited datasource', async () => {
|
||||
mockGetItem.mockResolvedValue(JSON.stringify(['prometheus-uid']));
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual(['prometheus-uid']);
|
||||
});
|
||||
|
||||
expect(result.current.isFavoriteDatasource('loki-uid')).toBe(false);
|
||||
expect(result.current.isFavoriteDatasource('elasticsearch-uid')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no favorites exist', async () => {
|
||||
mockGetItem.mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(() => useFavoriteDatasources());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.favoriteDatasources).toEqual([]);
|
||||
});
|
||||
|
||||
expect(result.current.isFavoriteDatasource('prometheus-uid')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
95
packages/grafana-runtime/src/utils/useFavoriteDatasources.ts
Normal file
95
packages/grafana-runtime/src/utils/useFavoriteDatasources.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { UserStorage } from './userStorage';
|
||||
|
||||
const FAVORITE_DATASOURCES_KEY = 'favoriteDatasources';
|
||||
|
||||
/**
|
||||
* A hook for managing favorite data sources using user storage.
|
||||
* This hook provides functionality to store and retrieve a list of favorite data source UIDs
|
||||
* using the backend user storage (with localStorage fallback).
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - An array of favorite data source UIDs
|
||||
* - An array of favorite data source UIDs that were initially loaded from storage
|
||||
* - A function to add a data source to favorites
|
||||
* - A function to remove a data source from favorites
|
||||
* - A function to check if a data source is favorited
|
||||
* @public
|
||||
*/
|
||||
export function useFavoriteDatasources(): {
|
||||
favoriteDatasources: string[];
|
||||
initialFavoriteDataSources: string[];
|
||||
addFavoriteDatasource: (ds: DataSourceInstanceSettings) => void;
|
||||
removeFavoriteDatasource: (ds: DataSourceInstanceSettings) => void;
|
||||
isFavoriteDatasource: (dsUid: string) => boolean;
|
||||
} {
|
||||
const [userStorage] = useState(() => new UserStorage('grafana-runtime'));
|
||||
const [favoriteDatasources, setFavoriteDatasources] = useState<string[]>([]);
|
||||
const [initialFavoriteDataSources, setInitialFavoriteDataSources] = useState<string[]>([]);
|
||||
|
||||
// Load favorites from storage on mount
|
||||
useEffect(() => {
|
||||
const loadFavorites = async () => {
|
||||
const stored = await userStorage.getItem(FAVORITE_DATASOURCES_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
setFavoriteDatasources(parsed);
|
||||
setInitialFavoriteDataSources(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
loadFavorites();
|
||||
}, [userStorage]);
|
||||
|
||||
// Helper function to save favorites to storage
|
||||
const saveFavorites = useCallback(
|
||||
async (newFavorites: string[]) => {
|
||||
await userStorage.setItem(FAVORITE_DATASOURCES_KEY, JSON.stringify(newFavorites));
|
||||
setFavoriteDatasources(newFavorites);
|
||||
},
|
||||
[userStorage]
|
||||
);
|
||||
|
||||
const addFavoriteDatasource = useCallback(
|
||||
(ds: DataSourceInstanceSettings) => {
|
||||
if (ds.meta.builtIn) {
|
||||
// Prevent storing built-in datasources (-- Grafana --, -- Mixed --, -- Dashboard --)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!favoriteDatasources.includes(ds.uid)) {
|
||||
const newFavorites = [...favoriteDatasources, ds.uid];
|
||||
saveFavorites(newFavorites);
|
||||
}
|
||||
},
|
||||
[favoriteDatasources, saveFavorites]
|
||||
);
|
||||
|
||||
const removeFavoriteDatasource = useCallback(
|
||||
(ds: DataSourceInstanceSettings) => {
|
||||
const newFavorites = favoriteDatasources.filter((uid) => uid !== ds.uid);
|
||||
if (newFavorites.length !== favoriteDatasources.length) {
|
||||
saveFavorites(newFavorites);
|
||||
}
|
||||
},
|
||||
[favoriteDatasources, saveFavorites]
|
||||
);
|
||||
|
||||
const isFavoriteDatasource = useCallback(
|
||||
(dsUid: string) => {
|
||||
return favoriteDatasources.includes(dsUid);
|
||||
},
|
||||
[favoriteDatasources]
|
||||
);
|
||||
|
||||
return {
|
||||
favoriteDatasources,
|
||||
addFavoriteDatasource,
|
||||
removeFavoriteDatasource,
|
||||
isFavoriteDatasource,
|
||||
initialFavoriteDataSources,
|
||||
};
|
||||
}
|
@ -1,16 +1,26 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, TagList, useTheme2 } from '@grafana/ui';
|
||||
import { Card, TagList, useTheme2, Icon } from '@grafana/ui';
|
||||
|
||||
interface DataSourceCardProps {
|
||||
ds: DataSourceInstanceSettings;
|
||||
onClick: () => void;
|
||||
selected: boolean;
|
||||
description?: string;
|
||||
isFavorite?: boolean;
|
||||
onToggleFavorite?: (ds: DataSourceInstanceSettings) => void;
|
||||
}
|
||||
|
||||
export function DataSourceCard({ ds, onClick, selected, description, ...htmlProps }: DataSourceCardProps) {
|
||||
export function DataSourceCard({
|
||||
ds,
|
||||
onClick,
|
||||
selected,
|
||||
description,
|
||||
isFavorite = false,
|
||||
onToggleFavorite,
|
||||
...htmlProps
|
||||
}: DataSourceCardProps) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, ds.meta.builtIn);
|
||||
|
||||
@ -26,7 +36,19 @@ export function DataSourceCard({ ds, onClick, selected, description, ...htmlProp
|
||||
<span className={styles.name}>
|
||||
{ds.name} {ds.isDefault ? <TagList tags={['default']} /> : null}
|
||||
</span>
|
||||
<div className={styles.rightSection}>
|
||||
<small className={styles.type}>{description || ds.meta.name}</small>
|
||||
{onToggleFavorite && !ds.meta.builtIn && (
|
||||
<Icon
|
||||
name={isFavorite ? 'favorite' : 'star'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite(ds);
|
||||
}}
|
||||
className={styles.favoriteButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Heading>
|
||||
<Card.Figure className={styles.logo}>
|
||||
@ -66,6 +88,17 @@ function getStyles(theme: GrafanaTheme2, builtIn = false) {
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
rightSection: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}),
|
||||
logo: css({
|
||||
width: '32px',
|
||||
@ -92,6 +125,11 @@ function getStyles(theme: GrafanaTheme2, builtIn = false) {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
favoriteButton: css({
|
||||
flexShrink: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 1,
|
||||
}),
|
||||
separator: css({
|
||||
margin: theme.spacing(0, 1),
|
||||
color: theme.colors.border.weak,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { config, getTemplateSrv, useFavoriteDatasources } from '@grafana/runtime';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { useDatasources, useKeyboardNavigatableList, useRecentlyUsedDataSources } from '../../hooks';
|
||||
@ -74,6 +74,27 @@ export function DataSourceList(props: DataSourceListProps) {
|
||||
});
|
||||
|
||||
const [recentlyUsedDataSources, pushRecentlyUsedDataSource] = useRecentlyUsedDataSources();
|
||||
|
||||
const favoriteDataSourcesHook = config.featureToggles.favoriteDatasources ? useFavoriteDatasources() : null;
|
||||
const storedFavoriteDataSources = favoriteDataSourcesHook?.initialFavoriteDataSources;
|
||||
const isFavoriteDatasource = favoriteDataSourcesHook?.isFavoriteDatasource;
|
||||
|
||||
const toggleFavoriteDatasource = useCallback(
|
||||
(ds: DataSourceInstanceSettings) => {
|
||||
if (!favoriteDataSourcesHook) {
|
||||
return;
|
||||
}
|
||||
const { isFavoriteDatasource, addFavoriteDatasource, removeFavoriteDatasource } = favoriteDataSourcesHook;
|
||||
|
||||
if (isFavoriteDatasource(ds.uid)) {
|
||||
removeFavoriteDatasource(ds);
|
||||
} else {
|
||||
addFavoriteDatasource(ds);
|
||||
}
|
||||
},
|
||||
[favoriteDataSourcesHook]
|
||||
);
|
||||
|
||||
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
|
||||
|
||||
return (
|
||||
@ -86,7 +107,14 @@ export function DataSourceList(props: DataSourceListProps) {
|
||||
<EmptyState className={styles.emptyState} onClickCTA={onClickEmptyStateCTA} />
|
||||
)}
|
||||
{filteredDataSources
|
||||
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
||||
.sort(
|
||||
getDataSourceCompareFn(
|
||||
current,
|
||||
recentlyUsedDataSources,
|
||||
getDataSourceVariableIDs(),
|
||||
storedFavoriteDataSources
|
||||
)
|
||||
)
|
||||
.map((ds) => (
|
||||
<DataSourceCard
|
||||
data-testid="data-source-card"
|
||||
@ -97,6 +125,8 @@ export function DataSourceList(props: DataSourceListProps) {
|
||||
onChange(ds);
|
||||
}}
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
isFavorite={isFavoriteDatasource ? isFavoriteDatasource(ds.uid) : undefined}
|
||||
onToggleFavorite={toggleFavoriteDatasource}
|
||||
{...(enableKeyboardNavigation ? navigatableProps : {})}
|
||||
/>
|
||||
))}
|
||||
|
@ -77,6 +77,16 @@ describe('getDataSouceCompareFn', () => {
|
||||
] as DataSourceInstanceSettings[]);
|
||||
});
|
||||
|
||||
it('sorts favorite 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 } },
|
||||
{ uid: 'D', name: 'D', meta: { builtIn: false } },
|
||||
] as DataSourceInstanceSettings[]);
|
||||
});
|
||||
|
||||
it('sorts variables before other datasources', () => {
|
||||
dataSources.sort(getDataSourceCompareFn(undefined, [], ['c', 'b']));
|
||||
expect(dataSources).toEqual([
|
||||
@ -87,7 +97,7 @@ describe('getDataSouceCompareFn', () => {
|
||||
] as DataSourceInstanceSettings[]);
|
||||
});
|
||||
|
||||
it('sorts datasources current -> recently used -> variables -> others -> built in', () => {
|
||||
it('sorts datasources current -> favorite -> recently used -> variables -> others -> built in', () => {
|
||||
const dataSources = [
|
||||
{ uid: 'a', name: 'a', meta: { builtIn: true } },
|
||||
{ uid: 'b', name: 'b', meta: { builtIn: false } },
|
||||
@ -97,13 +107,13 @@ describe('getDataSouceCompareFn', () => {
|
||||
{ uid: 'f', name: 'f', meta: { builtIn: false } },
|
||||
] as DataSourceInstanceSettings[];
|
||||
|
||||
dataSources.sort(getDataSourceCompareFn('c', ['b', 'e'], ['d']));
|
||||
dataSources.sort(getDataSourceCompareFn('c', ['b', 'e'], ['d'], ['f']));
|
||||
expect(dataSources).toEqual([
|
||||
{ uid: 'c', name: 'c', meta: { builtIn: false } },
|
||||
{ uid: 'f', name: 'f', 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[]);
|
||||
});
|
||||
|
@ -48,7 +48,8 @@ export function dataSourceLabel(
|
||||
export function getDataSourceCompareFn(
|
||||
current: DataSourceRef | DataSourceInstanceSettings | string | null | undefined,
|
||||
recentlyUsedDataSources: string[],
|
||||
dataSourceVariablesIDs: string[]
|
||||
dataSourceVariablesIDs: string[],
|
||||
favoriteDatasources: string[] = []
|
||||
) {
|
||||
const cmpDataSources = (a: DataSourceInstanceSettings, b: DataSourceInstanceSettings) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
@ -61,7 +62,19 @@ export function getDataSourceCompareFn(
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sort recently used data sources by latest used, but after current.
|
||||
// Sort favorite data sources after current but before recently used.
|
||||
const aIsFavorite = favoriteDatasources.includes(a.uid);
|
||||
const bIsFavorite = favoriteDatasources.includes(b.uid);
|
||||
if (aIsFavorite && !bIsFavorite) {
|
||||
return -1;
|
||||
} else if (bIsFavorite && !aIsFavorite) {
|
||||
return 1;
|
||||
} else if (aIsFavorite && bIsFavorite) {
|
||||
// If both are favorites, sort alphabetically by name.
|
||||
return nameA < nameB ? -1 : 1;
|
||||
}
|
||||
|
||||
// Sort recently used data sources by latest used, but after current and favorites.
|
||||
const aIndex = recentlyUsedDataSources.indexOf(a.uid);
|
||||
const bIndex = recentlyUsedDataSources.indexOf(b.uid);
|
||||
if (aIndex > -1 && aIndex > bIndex) {
|
||||
|
Reference in New Issue
Block a user