mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
ea8b3267e5
commit
bbe9c8661a
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
|
@ -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', () => {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user