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
This commit is contained in:
Kristina 2024-02-23 09:44:21 -06:00 committed by GitHub
parent ea8b3267e5
commit bbe9c8661a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 295 additions and 155 deletions

View File

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

View File

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

View File

@ -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();

View File

@ -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<TQuery extends DataQuery>(queries: TQuery[]): b
);
}
/**
* Update the query history. Side-effect: store history in local storage
*/
export function updateHistory<T extends DataQuery>(
history: Array<HistoryItem<T>>,
datasourceId: string,
queries: T[]
): Array<HistoryItem<T>> {
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<string[]>((newQueryKeys, query, index) => {
const primaryKey = query.datasource?.uid || query.key;

View File

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

View File

@ -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(

View File

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

View File

@ -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 (
<div className={styles.container}>
@ -174,13 +178,13 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
<div className={styles.labelSlider}>
<Trans i18nKey="explore.rich-history-queries-tab.filter-history">Filter history</Trans>
</div>
<div className={styles.labelSlider}>{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}</div>
<div className={styles.labelSlider}>{mapNumbertoTimeInSlider(timeFilter[0])}</div>
<div className={styles.slider}>
<RangeSlider
tooltipAlwaysVisible={false}
min={0}
max={richHistorySettings.retentionPeriod}
value={[richHistorySearchFilters.from, richHistorySearchFilters.to]}
value={timeFilter}
orientation="vertical"
formatTooltipResult={mapNumbertoTimeInSlider}
reverse={true}
@ -189,7 +193,7 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
}}
/>
</div>
<div className={styles.labelSlider}>{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}</div>
<div className={styles.labelSlider}>{mapNumbertoTimeInSlider(timeFilter[1])}</div>
</div>
</div>

View File

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

View File

@ -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<HistoryUpdatedPayload>('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<void> => {
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;
};

View File

@ -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<StoreState>);
};
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', () => {

View File

@ -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<HistoryItem<DataQuery>>,
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<void, RunQueriesOptions>(
}));
if (datasourceInstance != null) {
handleHistory(dispatch, getState().explore, exploreItemState.history, datasourceInstance, queries, exploreId);
handleHistory(dispatch, getState().explore, datasourceInstance, queries);
}
const cachedValue = getResultsFromCache(cache, absoluteRange);

View File

@ -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', () => {

View File

@ -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<DataQuery, DataSourceJsonData, {}>;
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<HistoryItem[]>(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<HistoryItem[]>(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) {