From bbe9c8661a75d514f4a76fe43dc2f2f34413b1d2 Mon Sep 17 00:00:00 2001 From: Kristina Date: Fri, 23 Feb 2024 09:44:21 -0600 Subject: [PATCH] Explore: Use rich history local storage for autocomplete (#81386) * move autocomplete logic * Tests * Write to correct history * Remove historyUpdatedAction and related code * add helpful comments * Add option to mute all errors/warnings for autocomplete * Add back in legacy local storage query history for transition period * Move params to an object for easier use and defaults * Do not make time filter required * fix tests * change deprecation version and add issue number --- .../core/history/RichHistoryLocalStorage.ts | 14 ++- .../history/richHistoryStorageProvider.ts | 6 + public/app/core/utils/explore.test.ts | 23 ---- public/app/core/utils/explore.ts | 32 ----- public/app/core/utils/richHistory.test.ts | 32 +++-- public/app/core/utils/richHistory.ts | 38 ++++-- public/app/core/utils/richHistoryTypes.ts | 4 +- .../RichHistory/RichHistoryQueriesTab.tsx | 10 +- .../app/features/explore/state/explorePane.ts | 2 - public/app/features/explore/state/history.ts | 59 ++++----- .../app/features/explore/state/query.test.ts | 25 ++++ public/app/features/explore/state/query.ts | 24 ++-- .../app/features/explore/state/utils.test.ts | 115 +++++++++++++++++- public/app/features/explore/state/utils.ts | 66 ++++++++-- 14 files changed, 295 insertions(+), 155 deletions(-) diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 737246ca301..ab84d420d49 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -37,10 +37,16 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { const allQueries = getRichHistoryDTOs().map(fromDTO); const queries = filters.starred ? allQueries.filter((q) => q.starred === true) : allQueries; - const richHistory = filterAndSortQueries(queries, filters.sortOrder, filters.datasourceFilters, filters.search, [ - filters.from, - filters.to, - ]); + const timeFilter: [number, number] | undefined = + filters.from && filters.to ? [filters.from, filters.to] : undefined; + + const richHistory = filterAndSortQueries( + queries, + filters.sortOrder, + filters.datasourceFilters, + filters.search, + timeFilter + ); return { richHistory, total: richHistory.length }; } diff --git a/public/app/core/history/richHistoryStorageProvider.ts b/public/app/core/history/richHistoryStorageProvider.ts index 44337ffbb0e..84defd0f001 100644 --- a/public/app/core/history/richHistoryStorageProvider.ts +++ b/public/app/core/history/richHistoryStorageProvider.ts @@ -10,10 +10,16 @@ import RichHistoryStorage from './RichHistoryStorage'; const richHistoryLocalStorage = new RichHistoryLocalStorage(); const richHistoryRemoteStorage = new RichHistoryRemoteStorage(); +// for query history operations export const getRichHistoryStorage = (): RichHistoryStorage => { return config.queryHistoryEnabled ? richHistoryRemoteStorage : richHistoryLocalStorage; }; +// for autocomplete read and write operations +export const getLocalRichHistoryStorage = (): RichHistoryStorage => { + return richHistoryLocalStorage; +}; + interface RichHistorySupportedFeatures { availableFilters: SortOrder[]; lastUsedDataSourcesAvailable: boolean; diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 07a840f7a3e..658f3eefd0f 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -2,7 +2,6 @@ import { DataSourceApi, dateTime, ExploreUrlState, LogsSortOrder } from '@grafan import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { DataQuery } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; -import store from 'app/core/store'; import { DEFAULT_RANGE } from 'app/features/explore/state/utils'; import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv'; @@ -11,7 +10,6 @@ import { buildQueryTransaction, hasNonEmptyQuery, refreshIntervalToSortOrder, - updateHistory, getExploreUrl, GetExploreUrlArguments, getTimeRange, @@ -151,27 +149,6 @@ describe('getExploreUrl', () => { }); }); -describe('updateHistory()', () => { - const datasourceId = 'myDatasource'; - const key = `grafana.explore.history.${datasourceId}`; - - beforeEach(() => { - store.delete(key); - expect(store.exists(key)).toBeFalsy(); - }); - - test('should save history item to localStorage', () => { - const expected = [ - { - query: { refId: '1', expr: 'metric' }, - }, - ]; - expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected); - expect(store.exists(key)).toBeTruthy(); - expect(store.getObject(key)).toMatchObject(expected); - }); -}); - describe('hasNonEmptyQuery', () => { test('should return true if one query is non-empty', () => { expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy(); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7852a73a263..501a03d57d4 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -10,7 +10,6 @@ import { DataSourceApi, DataSourceRef, DefaultTimeZone, - HistoryItem, IntervalValues, LogsDedupStrategy, LogsSortOrder, @@ -37,8 +36,6 @@ export const DEFAULT_UI_STATE = { export const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; const nanoid = customAlphabet(ID_ALPHABET, 3); -const MAX_HISTORY_ITEMS = 100; - const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DATASOURCE_KEY}.${orgId}`; export const getLastUsedDatasourceUID = (orgId: number) => @@ -276,35 +273,6 @@ export function hasNonEmptyQuery(queries: TQuery[]): b ); } -/** - * Update the query history. Side-effect: store history in local storage - */ -export function updateHistory( - history: Array>, - datasourceId: string, - queries: T[] -): Array> { - const ts = Date.now(); - let updatedHistory = history; - queries.forEach((query) => { - updatedHistory = [{ query, ts }, ...updatedHistory]; - }); - - if (updatedHistory.length > MAX_HISTORY_ITEMS) { - updatedHistory = updatedHistory.slice(0, MAX_HISTORY_ITEMS); - } - - // Combine all queries of a datasource type into one history - const historyKey = `grafana.explore.history.${datasourceId}`; - try { - store.setObject(historyKey, updatedHistory); - return updatedHistory; - } catch (error) { - console.error(error); - return history; - } -} - export const getQueryKeys = (queries: DataQuery[]): string[] => { const queryKeys = queries.reduce((newQueryKeys, query, index) => { const primaryKey = query.datasource?.uid || query.key; diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 4e87f38e748..0c6a95b981d 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -109,15 +109,13 @@ describe('richHistory', () => { it('should append query to query history', async () => { Date.now = jest.fn(() => 2); - const { limitExceeded, richHistoryStorageFull } = await addToRichHistory( - mock.testDatasourceUid, - mock.testDatasourceName, - mock.testQueries, - mock.testStarred, - mock.testComment, - true, - true - ); + const { limitExceeded, richHistoryStorageFull } = await addToRichHistory({ + localOverride: false, + datasource: { uid: mock.testDatasourceUid, name: mock.testDatasourceName }, + queries: mock.testQueries, + starred: mock.testStarred, + comment: mock.testComment, + }); expect(limitExceeded).toBeFalsy(); expect(richHistoryStorageFull).toBeFalsy(); expect(richHistoryStorageMock.addToRichHistory).toBeCalledWith({ @@ -142,15 +140,13 @@ describe('richHistory', () => { }); }); - const { richHistoryStorageFull, limitExceeded } = await addToRichHistory( - mock.testDatasourceUid, - mock.testDatasourceName, - mock.testQueries, - mock.testStarred, - mock.testComment, - true, - true - ); + const { richHistoryStorageFull, limitExceeded } = await addToRichHistory({ + localOverride: false, + datasource: { uid: mock.testDatasourceUid, name: mock.testDatasourceName }, + queries: mock.testQueries, + starred: mock.testStarred, + comment: mock.testComment, + }); expect(richHistoryStorageFull).toBeFalsy(); expect(limitExceeded).toBeTruthy(); }); diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 459e62b56d0..ad95f625696 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -15,7 +15,7 @@ import { RichHistoryStorageWarning, RichHistoryStorageWarningDetails, } from '../history/RichHistoryStorage'; -import { getRichHistoryStorage } from '../history/richHistoryStorageProvider'; +import { getLocalRichHistoryStorage, getRichHistoryStorage } from '../history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from './richHistoryTypes'; @@ -26,15 +26,27 @@ export { RichHistorySearchFilters, RichHistorySettings, SortOrder }; * Side-effect: store history in local storage */ +type addToRichHistoryParams = { + localOverride: boolean; + datasource: { uid: string; name?: string }; + queries: DataQuery[]; + starred: boolean; + comment?: string; + showNotif?: { + quotaExceededError?: boolean; + limitExceededWarning?: boolean; + otherErrors?: boolean; + }; +}; + export async function addToRichHistory( - datasourceUid: string, - datasourceName: string | null, - queries: DataQuery[], - starred: boolean, - comment: string | null, - showQuotaExceededError: boolean, - showLimitExceededWarning: boolean + params: addToRichHistoryParams ): Promise<{ richHistoryStorageFull?: boolean; limitExceeded?: boolean }> { + const { queries, localOverride, datasource, starred, comment, showNotif } = params; + // default showing of errors to true + const showQuotaExceededError = showNotif?.quotaExceededError ?? true; + const showLimitExceededWarning = showNotif?.limitExceededWarning ?? true; + const showOtherErrors = showNotif?.otherErrors ?? true; /* Save only queries, that are not falsy (e.g. empty object, null, ...) */ const newQueriesToSave: DataQuery[] = queries && queries.filter((query) => notEmptyQuery(query)); @@ -44,9 +56,11 @@ export async function addToRichHistory( let warning: RichHistoryStorageWarningDetails | undefined; try { - const result = await getRichHistoryStorage().addToRichHistory({ - datasourceUid: datasourceUid, - datasourceName: datasourceName ?? '', + // for autocomplete we want to ensure writing to local storage + const storage = localOverride ? getLocalRichHistoryStorage() : getRichHistoryStorage(); + const result = await storage.addToRichHistory({ + datasourceUid: datasource.uid, + datasourceName: datasource.name ?? '', queries: newQueriesToSave, starred, comment: comment ?? '', @@ -57,7 +71,7 @@ export async function addToRichHistory( if (error.name === RichHistoryServiceError.StorageFull) { richHistoryStorageFull = true; showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message))); - } else if (error.name !== RichHistoryServiceError.DuplicatedEntry) { + } else if (showOtherErrors && error.name !== RichHistoryServiceError.DuplicatedEntry) { dispatch( notifyApp( createErrorNotification( diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index b311b827529..c49029c035b 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -23,8 +23,8 @@ export type RichHistorySearchFilters = { sortOrder: SortOrder; /** Names of data sources (not uids) - used by local and remote storage **/ datasourceFilters: string[]; - from: number; - to: number; + from?: number; + to?: number; starred: boolean; page?: number; }; diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index e118d226356..7a7b5cc9d0e 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -166,6 +166,10 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) { const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder); const sortOrderOptions = getSortOrderOptions(); const partialResults = queries.length && queries.length !== totalQueries; + const timeFilter = [ + richHistorySearchFilters.from || 0, + richHistorySearchFilters.to || richHistorySettings.retentionPeriod, + ]; return (
@@ -174,13 +178,13 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
Filter history
-
{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}
+
{mapNumbertoTimeInSlider(timeFilter[0])}
-
{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}
+
{mapNumbertoTimeInSlider(timeFilter[1])}
diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index c426aaf690f..e6feeda2995 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -20,7 +20,6 @@ import { createAsyncThunk, ThunkResult } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; import { datasourceReducer } from './datasource'; -import { historyReducer } from './history'; import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main'; import { queryReducer, runQueries } from './query'; import { timeReducer, updateTime } from './time'; @@ -214,7 +213,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac state = queryReducer(state, action); state = datasourceReducer(state, action); state = timeReducer(state, action); - state = historyReducer(state, action); if (richHistoryUpdatedAction.match(action)) { const { richHistory, total } = action.payload.richHistoryResults; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index 8a02945ceb1..fa8348fd8dc 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -1,6 +1,3 @@ -import { AnyAction, createAction } from '@reduxjs/toolkit'; - -import { HistoryItem } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { addToRichHistory, @@ -26,16 +23,6 @@ import { } from './main'; import { selectPanesEntries } from './selectors'; -// -// Actions and Payloads -// - -export interface HistoryUpdatedPayload { - exploreId: string; - history: HistoryItem[]; -} -export const historyUpdatedAction = createAction('explore/historyUpdated'); - // // Action creators // @@ -74,25 +61,33 @@ const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemSta }; export const addHistoryItem = ( + localOverride: boolean, datasourceUid: string, datasourceName: string, - queries: DataQuery[] + queries: DataQuery[], + hideAllErrorsAndWarnings: boolean ): ThunkResult => { return async (dispatch, getState) => { - const { richHistoryStorageFull, limitExceeded } = await addToRichHistory( - datasourceUid, - datasourceName, + const showNotif = hideAllErrorsAndWarnings + ? { quotaExceededError: false, limitExceededWarning: false, otherErrors: false } + : { + quotaExceededError: !getState().explore.richHistoryStorageFull, + limitExceededWarning: !getState().explore.richHistoryLimitExceededWarningShown, + }; + const { richHistoryStorageFull, limitExceeded } = await addToRichHistory({ + localOverride, + datasource: { uid: datasourceUid, name: datasourceName }, queries, - false, - '', - !getState().explore.richHistoryStorageFull, - !getState().explore.richHistoryLimitExceededWarningShown - ); - if (richHistoryStorageFull) { - dispatch(richHistoryStorageFullAction()); - } - if (limitExceeded) { - dispatch(richHistoryLimitExceededAction()); + starred: false, + showNotif, + }); + if (!hideAllErrorsAndWarnings) { + if (richHistoryStorageFull) { + dispatch(richHistoryStorageFullAction()); + } + if (limitExceeded) { + dispatch(richHistoryLimitExceededAction()); + } } }; }; @@ -199,13 +194,3 @@ export const updateHistorySearchFilters = (exploreId: string, filters: RichHisto } }; }; - -export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => { - if (historyUpdatedAction.match(action)) { - return { - ...state, - history: action.payload.history, - }; - } - return state; -}; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 18bd129cba0..334270c8f68 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -16,10 +16,12 @@ import { SupplementaryQueryType, } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; +import config from 'app/core/config'; import { queryLogsSample, queryLogsVolume } from 'app/features/logs/logsModel'; import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; +import * as richHistory from '../../../core/utils/richHistory'; import { configureStore } from '../../../store/configureStore'; import { setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv'; import { makeLogs } from '../__mocks__/makeLogs'; @@ -155,6 +157,11 @@ describe('runQueries', () => { } as unknown as Partial); }; + beforeEach(() => { + config.queryHistoryEnabled = false; + jest.clearAllMocks(); + }); + it('should pass dataFrames to state even if there is error in response', async () => { const { dispatch, getState } = setupTests(); setupQueryResponse(getState()); @@ -202,6 +209,24 @@ describe('runQueries', () => { await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] })); expect(getState().explore.panes.left!.graphResult).toBeDefined(); }); + + it('should add history items to both local and remote storage with the flag enabled', async () => { + config.queryHistoryEnabled = true; + const { dispatch } = setupTests(); + jest.spyOn(richHistory, 'addToRichHistory'); + await dispatch(runQueries({ exploreId: 'left' })); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls).toHaveLength(2); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[0][0].localOverride).toBeTruthy(); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[1][0].localOverride).toBeFalsy(); + }); + + it('should add history items to local storage only with the flag disabled', async () => { + const { dispatch } = setupTests(); + jest.spyOn(richHistory, 'addToRichHistory'); + await dispatch(runQueries({ exploreId: 'left' })); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls).toHaveLength(1); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[0][0].localOverride).toBeTruthy(); + }); }); describe('running queries', () => { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index b2b123fa22b..320f1b47108 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -13,7 +13,6 @@ import { dateTimeForTimeZone, hasQueryExportSupport, hasQueryImportSupport, - HistoryItem, LoadingState, LogsVolumeType, PanelEvents, @@ -35,7 +34,6 @@ import { getTimeRange, hasNonEmptyQuery, stopQueryState, - updateHistory, } from 'app/core/utils/explore'; import { getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; @@ -67,7 +65,7 @@ import { import { getCorrelations } from './correlations'; import { saveCorrelationsAction } from './explorePane'; -import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history'; +import { addHistoryItem, loadRichHistory } from './history'; import { changeCorrelationEditorDetails } from './main'; import { updateTime } from './time'; import { @@ -481,16 +479,18 @@ export function modifyQueries( async function handleHistory( dispatch: ThunkDispatch, state: ExploreState, - history: Array>, datasource: DataSourceApi, - queries: DataQuery[], - exploreId: string + queries: DataQuery[] ) { - const datasourceId = datasource.meta.id; - const nextHistory = updateHistory(history, datasourceId, queries); - dispatch(historyUpdatedAction({ exploreId, history: nextHistory })); - - dispatch(addHistoryItem(datasource.uid, datasource.name, queries)); + /* + Always write to local storage. If query history is enabled, we will use local storage for autocomplete only (and want to hide errors) + If query history is disabled, we will use local storage for query history as well, and will want to show errors + */ + dispatch(addHistoryItem(true, datasource.uid, datasource.name, queries, config.queryHistoryEnabled)); + if (config.queryHistoryEnabled) { + // write to remote if flag enabled + dispatch(addHistoryItem(false, datasource.uid, datasource.name, queries, false)); + } // Because filtering happens in the backend we cannot add a new entry without checking if it matches currently // used filters. Instead, we refresh the query history list. @@ -550,7 +550,7 @@ export const runQueries = createAsyncThunk( })); if (datasourceInstance != null) { - handleHistory(dispatch, getState().explore, exploreItemState.history, datasourceInstance, queries, exploreId); + handleHistory(dispatch, getState().explore, datasourceInstance, queries); } const cachedValue = getResultsFromCache(cache, absoluteRange); diff --git a/public/app/features/explore/state/utils.test.ts b/public/app/features/explore/state/utils.test.ts index e58c1fe4b43..e044c647287 100644 --- a/public/app/features/explore/state/utils.test.ts +++ b/public/app/features/explore/state/utils.test.ts @@ -1,6 +1,9 @@ import { dateTime } from '@grafana/data'; +import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; import * as exploreUtils from 'app/core/utils/explore'; +import { loadAndInitDatasource, getRange, fromURLRange, MAX_HISTORY_AUTOCOMPLETE_ITEMS } from './utils'; + const dataSourceMock = { get: jest.fn(), }; @@ -8,7 +11,15 @@ jest.mock('app/features/plugins/datasource_srv', () => ({ getDatasourceSrv: jest.fn(() => dataSourceMock), })); -import { loadAndInitDatasource, getRange, fromURLRange } from './utils'; +const mockLocalDataStorage = { + getRichHistory: jest.fn(), +}; + +jest.mock('app/core/history/richHistoryStorageProvider', () => ({ + getLocalRichHistoryStorage: jest.fn(() => { + return mockLocalDataStorage; + }), +})); const DEFAULT_DATASOURCE = { uid: 'abc123', name: 'Default' }; const TEST_DATASOURCE = { uid: 'def789', name: 'Test' }; @@ -28,6 +39,7 @@ describe('loadAndInitDatasource', () => { setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); dataSourceMock.get.mockRejectedValueOnce(new Error('Datasource not found')); dataSourceMock.get.mockResolvedValue(DEFAULT_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValue({ total: 0, richHistory: [] }); const { instance } = await loadAndInitDatasource(1, { uid: 'Unknown' }); @@ -41,14 +53,111 @@ describe('loadAndInitDatasource', () => { it('saves last loaded data source uid', async () => { setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValue({ + total: 0, + richHistory: [], + }); const { instance } = await loadAndInitDatasource(1, { uid: 'Test' }); - expect(dataSourceMock.get).toBeCalledTimes(1); - expect(dataSourceMock.get).toBeCalledWith({ uid: 'Test' }); + expect(dataSourceMock.get).toHaveBeenCalledTimes(1); + expect(dataSourceMock.get).toHaveBeenCalledWith({ uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(instance).toMatchObject(TEST_DATASOURCE); expect(setLastUsedDatasourceUIDSpy).toBeCalledWith(1, TEST_DATASOURCE.uid); }); + + it('pulls history data and returns the history by query', async () => { + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: [{ refId: 'A' }, { refId: 'B' }], + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(2); + }); + + it('pulls history data and returns the history by query with Mixed results', async () => { + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: [{ refId: 'A' }, { refId: 'B' }], + }, + ], + }); + + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Mixed', + datasourceName: 'Mixed', + starred: false, + comment: '', + queries: [ + { refId: 'A', datasource: { uid: 'def789' } }, + { refId: 'B', datasource: { uid: 'def789' } }, + ], + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(4); + }); + + it('pulls history data and returns only a max of MAX_HISTORY_AUTOCOMPLETE_ITEMS items', async () => { + const queryList = [...Array(MAX_HISTORY_AUTOCOMPLETE_ITEMS + 50).keys()].map((i) => { + return { refId: `ref-${i}` }; + }); + + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: queryList, + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(MAX_HISTORY_AUTOCOMPLETE_ITEMS); + }); }); describe('getRange', () => { diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 616ba220c88..0918049c03e 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -21,16 +21,20 @@ import { URLRangeValue, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { DataQuery, DataSourceRef, TimeZone } from '@grafana/schema'; +import { DataQuery, DataSourceJsonData, DataSourceRef, TimeZone } from '@grafana/schema'; +import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; +import { SortOrder } from 'app/core/utils/richHistory'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ExplorePanelData, StoreState } from 'app/types'; -import { ExploreItemState } from 'app/types/explore'; +import { ExploreItemState, RichHistoryQuery } from 'app/types/explore'; import store from '../../../core/store'; import { setLastUsedDatasourceUID } from '../../../core/utils/explore'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; +export const MAX_HISTORY_AUTOCOMPLETE_ITEMS = 100; + export const DEFAULT_RANGE = { from: 'now-1h', to: 'now', @@ -100,7 +104,7 @@ export async function loadAndInitDatasource( orgId: number, datasource: DataSourceRef | string ): Promise<{ history: HistoryItem[]; instance: DataSourceApi }> { - let instance; + let instance: DataSourceApi; try { // let datasource be a ref if we have the info, otherwise a name or uid will do for lookup instance = await getDatasourceSrv().get(datasource); @@ -119,12 +123,60 @@ export async function loadAndInitDatasource( } } - const historyKey = `grafana.explore.history.${instance.meta?.id}`; - const history = store.getObject(historyKey, []); - // Save last-used datasource + let history: HistoryItem[] = []; + const localStorageHistory = getLocalRichHistoryStorage(); + + const historyResults = await localStorageHistory.getRichHistory({ + search: '', + sortOrder: SortOrder.Ascending, + datasourceFilters: [instance.name], + starred: false, + }); + + // first, fill autocomplete with query history for that datasource + if ((historyResults.total || 0) > 0) { + historyResults.richHistory.forEach((historyResult: RichHistoryQuery) => { + historyResult.queries.forEach((q) => { + history.push({ ts: parseInt(historyResult.id, 10), query: q }); + }); + }); + } + + if (history.length < MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + // check the last 100 mixed history results seperately + const historyMixedResults = await localStorageHistory.getRichHistory({ + search: '', + sortOrder: SortOrder.Ascending, + datasourceFilters: [MIXED_DATASOURCE_NAME], + starred: false, + }); + + if ((historyMixedResults.total || 0) > 0) { + // second, fill autocomplete with queries for that datasource used in Mixed scenarios + historyMixedResults.richHistory.forEach((historyResult: RichHistoryQuery) => { + historyResult.queries.forEach((q) => { + if (q?.datasource?.uid === instance.uid) { + history.push({ ts: parseInt(historyResult.id, 10), query: q }); + } + }); + }); + } + } + + // finally, add any legacy local storage history that might exist. To be removed in Grafana 12 #83309 + if (history.length < MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + const historyKey = `grafana.explore.history.${instance.meta?.id}`; + history = [...history, ...store.getObject(historyKey, [])]; + } + + if (history.length > MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + history.length = MAX_HISTORY_AUTOCOMPLETE_ITEMS; + } + + // Save last-used datasource setLastUsedDatasourceUID(orgId, instance.uid); - return { history, instance }; + return { history: history, instance }; } export function createCacheKey(absRange: AbsoluteTimeRange) {