import { AnyAction, createAction } from '@reduxjs/toolkit'; import { DataQuery, HistoryItem } from '@grafana/data'; import { config, logError } from '@grafana/runtime'; import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils'; import store from 'app/core/store'; import { addToRichHistory, deleteAllFromRichHistory, deleteQueryInRichHistory, getRichHistory, getRichHistorySettings, LocalStorageMigrationStatus, migrateQueryHistoryFromLocalStorage, updateCommentInRichHistory, updateRichHistorySettings, updateStarredInRichHistory, } from 'app/core/utils/richHistory'; import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; import { richHistoryLimitExceededAction, richHistoryMigrationFailedAction, richHistorySearchFiltersUpdatedAction, richHistorySettingsUpdatedAction, richHistoryStorageFullAction, richHistoryUpdatedAction, } from './main'; // // Actions and Payloads // export interface HistoryUpdatedPayload { exploreId: ExploreId; history: HistoryItem[]; } export const historyUpdatedAction = createAction('explore/historyUpdated'); // // Action creators // type SyncHistoryUpdatesOptions = { updatedQuery?: RichHistoryQuery; deletedId?: string; }; /** * Updates current state in both Explore panes after changing or deleting a query history item */ const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesOptions): ThunkResult => { return async (dispatch, getState) => { forEachExplorePane(getState().explore, (item, exploreId) => { const newRichHistory = item.richHistory // update .map((query) => (query.id === updatedQuery?.id ? updatedQuery : query)) // or remove .filter((query) => query.id !== deletedId); const deletedItems = item.richHistory.length - newRichHistory.length; dispatch( richHistoryUpdatedAction({ richHistoryResults: { richHistory: newRichHistory, total: item.richHistoryTotal! - deletedItems }, exploreId, }) ); }); }; }; const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: ExploreId) => void) => { callback(state.left, ExploreId.left); state.right && callback(state.right, ExploreId.right); }; export const addHistoryItem = ( datasourceUid: string, datasourceName: string, queries: DataQuery[] ): ThunkResult => { return async (dispatch, getState) => { const { richHistoryStorageFull, limitExceeded } = await addToRichHistory( datasourceUid, datasourceName, queries, false, '', !getState().explore.richHistoryStorageFull, !getState().explore.richHistoryLimitExceededWarningShown ); if (richHistoryStorageFull) { dispatch(richHistoryStorageFullAction()); } if (limitExceeded) { dispatch(richHistoryLimitExceededAction()); } }; }; export const starHistoryItem = (id: string, starred: boolean): ThunkResult => { return async (dispatch, getState) => { const updatedQuery = await updateStarredInRichHistory(id, starred); dispatch(updateRichHistoryState({ updatedQuery })); }; }; export const commentHistoryItem = (id: string, comment?: string): ThunkResult => { return async (dispatch) => { const updatedQuery = await updateCommentInRichHistory(id, comment); dispatch(updateRichHistoryState({ updatedQuery })); }; }; export const deleteHistoryItem = (id: string): ThunkResult => { return async (dispatch) => { const deletedId = await deleteQueryInRichHistory(id); dispatch(updateRichHistoryState({ deletedId })); }; }; export const deleteRichHistory = (): ThunkResult => { return async (dispatch) => { await deleteAllFromRichHistory(); dispatch( richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.left }) ); dispatch( richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.right }) ); }; }; export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { return async (dispatch, getState) => { const filters = getState().explore![exploreId]?.richHistorySearchFilters; if (filters) { const richHistoryResults = await getRichHistory(filters); dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId })); } }; }; export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult => { return async (dispatch, getState) => { const currentFilters = getState().explore![exploreId]?.richHistorySearchFilters; const currentRichHistory = getState().explore![exploreId]?.richHistory; if (currentFilters && currentRichHistory) { const nextFilters = { ...currentFilters, page: (currentFilters?.page || 1) + 1 }; const moreRichHistory = await getRichHistory(nextFilters); const richHistory = [...currentRichHistory, ...moreRichHistory.richHistory]; dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters, exploreId })); dispatch( richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total }, exploreId }) ); } }; }; export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult => { return async (dispatch) => { dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId })); dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId })); }; }; /** * Initialize query history pane. To load history it requires settings to be loaded first * (but only once per session). Filters are initialised by the tab (starred or home). */ export const initRichHistory = (): ThunkResult => { return async (dispatch, getState) => { const queriesMigrated = store.getBool(RICH_HISTORY_SETTING_KEYS.migrated, false); const migrationFailedDuringThisSession = getState().explore.richHistoryMigrationFailed; // Query history migration should always be successful, but in case of unexpected errors we ensure // the migration attempt happens only once per session, and the user is informed about the failure // in a way that can help with potential investigation. if (config.queryHistoryEnabled && !queriesMigrated && !migrationFailedDuringThisSession) { const migrationResult = await migrateQueryHistoryFromLocalStorage(); if (migrationResult.status === LocalStorageMigrationStatus.Failed) { dispatch(richHistoryMigrationFailedAction()); logError(migrationResult.error!, { explore: { event: 'QueryHistoryMigrationFailed' } }); } else { store.set(RICH_HISTORY_SETTING_KEYS.migrated, true); } } let settings = getState().explore.richHistorySettings; if (!settings) { settings = await getRichHistorySettings(); dispatch(richHistorySettingsUpdatedAction(settings)); } }; }; export const updateHistorySettings = (settings: RichHistorySettings): ThunkResult => { return async (dispatch) => { dispatch(richHistorySettingsUpdatedAction(settings)); await updateRichHistorySettings(settings); }; }; /** * Assumed this can be called only when settings and filters are initialised */ export const updateHistorySearchFilters = ( exploreId: ExploreId, filters: RichHistorySearchFilters ): ThunkResult => { return async (dispatch, getState) => { await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } })); const currentSettings = getState().explore.richHistorySettings!; if (supportedFeatures().lastUsedDataSourcesAvailable) { await dispatch( updateHistorySettings({ ...currentSettings, lastUsedDatasourceFilters: filters.datasourceFilters, }) ); } }; }; export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => { if (historyUpdatedAction.match(action)) { return { ...state, history: action.payload.history, }; } return state; };