From 3fda30194cedba897e3fa42ab3e8d9d59c65fd06 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:17:44 +0200 Subject: [PATCH] Dashboard: Select the last used data source by default when adding a panel to a dashboard (#71777) Co-authored-by: Juan Cabanas --- .../PanelEditor/PanelEditorQueries.tsx | 38 ++++++++- .../SaveDashboard/useDashboardSave.tsx | 6 ++ .../dashboard/utils/dashboard.test.ts | 78 +++++++++++++++++++ .../app/features/dashboard/utils/dashboard.ts | 57 ++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard/utils/dashboard.test.ts diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 7788b448276..7720a5cc5b7 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -4,9 +4,16 @@ import { DataQuery, getDataSourceRef } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; +import { getDashboardSrv } from '../../services/DashboardSrv'; import { PanelModel } from '../../state'; +import { + getLastUsedDatasourceFromStorage, + initLastUsedDatasourceKeyForDashboard, + setLastUsedDatasourceKeyForDashboard, +} from '../../utils/dashboard'; interface Props { /** Current panel */ @@ -20,12 +27,28 @@ export class PanelEditorQueries extends PureComponent { super(props); } + // store last used datasource in local storage + updateLastUsedDatasource = (datasource: QueryGroupDataSource) => { + if (!datasource.uid) { + return; + } + + const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? ''; + // if datasource is MIXED reset datasource uid in storage, because Mixed datasource can contain multiple ds + if (datasource.uid === MIXED_DATASOURCE_NAME) { + return initLastUsedDatasourceKeyForDashboard(dashboardUid!); + } + setLastUsedDatasourceKeyForDashboard(dashboardUid, datasource.uid); + }; + buildQueryOptions(panel: PanelModel): QueryGroupOptions { const dataSource: QueryGroupDataSource = panel.datasource ?? { default: true, }; const datasourceSettings = getDatasourceSrv().getInstanceSettings(dataSource); + // store last datasource used in local storage + this.updateLastUsedDatasource(dataSource); return { cacheTimeout: datasourceSettings?.meta.queryOptions?.cacheTimeout ? panel.cacheTimeout : undefined, dataSource: { @@ -51,7 +74,20 @@ export class PanelEditorQueries extends PureComponent { // If the panel model has no datasource property load the default data source property and update the persisted model // Because this part of the panel model is not in redux yet we do a forceUpdate. if (!panel.datasource) { - const ds = getDatasourceSrv().getInstanceSettings(null); + let ds; + // check if we have last used datasource from local storage + // get dashboard uid + const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? ''; + const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashboardUid!); + // do we have a last used datasource for this dashboard + if (lastUsedDatasource?.datasourceUid !== null) { + // get datasource from uid + ds = getDatasourceSrv().getInstanceSettings(lastUsedDatasource?.datasourceUid); + } + // else load default datasource + if (!ds) { + ds = getDatasourceSrv().getInstanceSettings(null); + } panel.datasource = getDataSourceRef(ds!); this.forceUpdate(); } diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index aedfa43c0b9..46bed9862c0 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -12,6 +12,8 @@ import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashb import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { updateDashboardUidLastUsedDatasource } from '../../utils/dashboard'; + import { SaveDashboardOptions } from './types'; const saveDashboard = async ( @@ -60,6 +62,10 @@ export const useDashboardSave = (dashboard: DashboardModel, isCopy = false) => { // important that these happen before location redirect below appEvents.publish(new DashboardSavedEvent()); notifyApp.success('Dashboard saved'); + + //Update local storage dashboard to handle things like last used datasource + updateDashboardUidLastUsedDatasource(result.uid); + if (isCopy) { reportInteraction('grafana_dashboard_copied', { name: dashboard.title, diff --git a/public/app/features/dashboard/utils/dashboard.test.ts b/public/app/features/dashboard/utils/dashboard.test.ts new file mode 100644 index 00000000000..ec9c3733038 --- /dev/null +++ b/public/app/features/dashboard/utils/dashboard.test.ts @@ -0,0 +1,78 @@ +import { + updateDashboardUidLastUsedDatasource, + getLastUsedDatasourceFromStorage, + initLastUsedDatasourceKeyForDashboard, + setLastUsedDatasourceKeyForDashboard, +} from './dashboard'; // Replace with the path to your actual module + +// Mock the store module +jest.mock('app/core/store', () => ({ + exists: jest.fn(), + getObject: jest.fn(), + setObject: jest.fn(), +})); + +const store = jest.requireMock('app/core/store'); + +describe('Last Used Datasource Local Storage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should retrieve the last used datasource', () => { + store.exists.mockReturnValue(true); + store.getObject.mockReturnValue({ + dashboardUid: '123', + datasourceUid: 'datasource1', + }); + + const result = getLastUsedDatasourceFromStorage('123'); + expect(result).toEqual({ dashboardUid: '123', datasourceUid: 'datasource1' }); + }); + + it('should update only the dashboard UID', () => { + store.exists.mockReturnValue(true); + store.getObject.mockReturnValue({ + dashboardUid: '456', + datasourceUid: 'datasource2', + }); + + updateDashboardUidLastUsedDatasource('789'); + expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: '789', + datasourceUid: 'datasource2', + }); + }); + + it('should initialize local storage for a dashboard with empty datasource UID', () => { + initLastUsedDatasourceKeyForDashboard('999'); + expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: '999', + datasourceUid: '', + }); + }); + + it('should set a new datasource UID and dashboard UID for a dashboard', () => { + store.exists.mockReturnValue(false); + + setLastUsedDatasourceKeyForDashboard('111', 'datasource3'); + expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: '111', + datasourceUid: 'datasource3', + }); + }); + + it('should update the datasource UID while keeping the existing dashboard UID', () => { + store.exists.mockReturnValue(true); + store.getObject.mockReturnValue({ + dashboardUid: '222', + datasourceUid: 'datasource4', + }); + + setLastUsedDatasourceKeyForDashboard('222', 'datasource5'); + expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: '222', + datasourceUid: 'datasource5', + }); + }); +}); diff --git a/public/app/features/dashboard/utils/dashboard.ts b/public/app/features/dashboard/utils/dashboard.ts index 9d1da06aded..f5768f31b9e 100644 --- a/public/app/features/dashboard/utils/dashboard.ts +++ b/public/app/features/dashboard/utils/dashboard.ts @@ -104,3 +104,60 @@ export function getCopiedPanelPlugin(): (PanelPluginMeta & PanelPluginInfo) | un return undefined; } + +type LastUsedDatasource = + | { + dashboardUid: string; + datasourceUid: string; + } + | undefined; + +const PANEL_EDIT_LAST_USED_DATASOURCE = 'grafana.dashboards.panelEdit.lastUsedDatasource'; + +// Function that returns last used datasource from local storage +export function getLastUsedDatasourceFromStorage(dashboardUid: string): LastUsedDatasource { + // Check if user has any local storage associated with this dashboard + if (store.exists(PANEL_EDIT_LAST_USED_DATASOURCE)) { + const lastUsedDatasource: LastUsedDatasource = store.getObject(PANEL_EDIT_LAST_USED_DATASOURCE); + if (lastUsedDatasource?.dashboardUid === dashboardUid) { + return lastUsedDatasource; + } + } + return undefined; +} + +// Function that updates local storage with new dashboard uid and keeps existing datasource +export function updateDashboardUidLastUsedDatasource(dashUid: string) { + // Check if user has any datasource uid in local storage + if (!store.exists(PANEL_EDIT_LAST_USED_DATASOURCE)) { + return; + } + const oldRegistryLastUsedDatasource: LastUsedDatasource = store.getObject(PANEL_EDIT_LAST_USED_DATASOURCE); + //keep existing datasource uid + const datasourceUid = oldRegistryLastUsedDatasource?.datasourceUid ?? ''; + updatePropsLastUsedDatasourceKey(dashUid, datasourceUid); +} + +// Function that updates local storage with new dashboard uid and resets datasource to empty +export function initLastUsedDatasourceKeyForDashboard(dashboardUid: string | undefined) { + store.setObject(PANEL_EDIT_LAST_USED_DATASOURCE, { dashboardUid: dashboardUid, datasourceUid: '' }); +} + +// Function that updates local storage with new datasource uid and keeps existing dashboard when there is dash uid key in local storage +// or sets new local storage with new dashboard uid and existing datasource +export function setLastUsedDatasourceKeyForDashboard(dashUid: string, dsUid: string) { + // Check if user has any datasource uid in local storage + const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashUid); + if (!lastUsedDatasource) { + updatePropsLastUsedDatasourceKey(dashUid, dsUid); + } else { + // set new local storage with new dashboard uid and existing datasource + const dashboardUid = lastUsedDatasource?.dashboardUid ?? ''; + updatePropsLastUsedDatasourceKey(dashboardUid, dsUid); + } +} + +// Function that updates local storage with new dashboard uid and datasource uid +function updatePropsLastUsedDatasourceKey(dashboardUid: string | undefined, datasourceUid: string) { + store.setObject(PANEL_EDIT_LAST_USED_DATASOURCE, { dashboardUid: dashboardUid, datasourceUid: datasourceUid }); +}