history.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { AnyAction, createAction } from '@reduxjs/toolkit';
  2. import { DataQuery, HistoryItem } from '@grafana/data';
  3. import { config, logError } from '@grafana/runtime';
  4. import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils';
  5. import store from 'app/core/store';
  6. import {
  7. addToRichHistory,
  8. deleteAllFromRichHistory,
  9. deleteQueryInRichHistory,
  10. getRichHistory,
  11. getRichHistorySettings,
  12. LocalStorageMigrationStatus,
  13. migrateQueryHistoryFromLocalStorage,
  14. updateCommentInRichHistory,
  15. updateRichHistorySettings,
  16. updateStarredInRichHistory,
  17. } from 'app/core/utils/richHistory';
  18. import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
  19. import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
  20. import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
  21. import {
  22. richHistoryLimitExceededAction,
  23. richHistoryMigrationFailedAction,
  24. richHistorySearchFiltersUpdatedAction,
  25. richHistorySettingsUpdatedAction,
  26. richHistoryStorageFullAction,
  27. richHistoryUpdatedAction,
  28. } from './main';
  29. //
  30. // Actions and Payloads
  31. //
  32. export interface HistoryUpdatedPayload {
  33. exploreId: ExploreId;
  34. history: HistoryItem[];
  35. }
  36. export const historyUpdatedAction = createAction<HistoryUpdatedPayload>('explore/historyUpdated');
  37. //
  38. // Action creators
  39. //
  40. type SyncHistoryUpdatesOptions = {
  41. updatedQuery?: RichHistoryQuery;
  42. deletedId?: string;
  43. };
  44. /**
  45. * Updates current state in both Explore panes after changing or deleting a query history item
  46. */
  47. const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesOptions): ThunkResult<void> => {
  48. return async (dispatch, getState) => {
  49. forEachExplorePane(getState().explore, (item, exploreId) => {
  50. const newRichHistory = item.richHistory
  51. // update
  52. .map((query) => (query.id === updatedQuery?.id ? updatedQuery : query))
  53. // or remove
  54. .filter((query) => query.id !== deletedId);
  55. const deletedItems = item.richHistory.length - newRichHistory.length;
  56. dispatch(
  57. richHistoryUpdatedAction({
  58. richHistoryResults: { richHistory: newRichHistory, total: item.richHistoryTotal! - deletedItems },
  59. exploreId,
  60. })
  61. );
  62. });
  63. };
  64. };
  65. const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: ExploreId) => void) => {
  66. callback(state.left, ExploreId.left);
  67. state.right && callback(state.right, ExploreId.right);
  68. };
  69. export const addHistoryItem = (
  70. datasourceUid: string,
  71. datasourceName: string,
  72. queries: DataQuery[]
  73. ): ThunkResult<void> => {
  74. return async (dispatch, getState) => {
  75. const { richHistoryStorageFull, limitExceeded } = await addToRichHistory(
  76. datasourceUid,
  77. datasourceName,
  78. queries,
  79. false,
  80. '',
  81. !getState().explore.richHistoryStorageFull,
  82. !getState().explore.richHistoryLimitExceededWarningShown
  83. );
  84. if (richHistoryStorageFull) {
  85. dispatch(richHistoryStorageFullAction());
  86. }
  87. if (limitExceeded) {
  88. dispatch(richHistoryLimitExceededAction());
  89. }
  90. };
  91. };
  92. export const starHistoryItem = (id: string, starred: boolean): ThunkResult<void> => {
  93. return async (dispatch, getState) => {
  94. const updatedQuery = await updateStarredInRichHistory(id, starred);
  95. dispatch(updateRichHistoryState({ updatedQuery }));
  96. };
  97. };
  98. export const commentHistoryItem = (id: string, comment?: string): ThunkResult<void> => {
  99. return async (dispatch) => {
  100. const updatedQuery = await updateCommentInRichHistory(id, comment);
  101. dispatch(updateRichHistoryState({ updatedQuery }));
  102. };
  103. };
  104. export const deleteHistoryItem = (id: string): ThunkResult<void> => {
  105. return async (dispatch) => {
  106. const deletedId = await deleteQueryInRichHistory(id);
  107. dispatch(updateRichHistoryState({ deletedId }));
  108. };
  109. };
  110. export const deleteRichHistory = (): ThunkResult<void> => {
  111. return async (dispatch) => {
  112. await deleteAllFromRichHistory();
  113. dispatch(
  114. richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.left })
  115. );
  116. dispatch(
  117. richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.right })
  118. );
  119. };
  120. };
  121. export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
  122. return async (dispatch, getState) => {
  123. const filters = getState().explore![exploreId]?.richHistorySearchFilters;
  124. if (filters) {
  125. const richHistoryResults = await getRichHistory(filters);
  126. dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId }));
  127. }
  128. };
  129. };
  130. export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
  131. return async (dispatch, getState) => {
  132. const currentFilters = getState().explore![exploreId]?.richHistorySearchFilters;
  133. const currentRichHistory = getState().explore![exploreId]?.richHistory;
  134. if (currentFilters && currentRichHistory) {
  135. const nextFilters = { ...currentFilters, page: (currentFilters?.page || 1) + 1 };
  136. const moreRichHistory = await getRichHistory(nextFilters);
  137. const richHistory = [...currentRichHistory, ...moreRichHistory.richHistory];
  138. dispatch(richHistorySearchFiltersUpdatedAction({ filters: nextFilters, exploreId }));
  139. dispatch(
  140. richHistoryUpdatedAction({ richHistoryResults: { richHistory, total: moreRichHistory.total }, exploreId })
  141. );
  142. }
  143. };
  144. };
  145. export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult<void> => {
  146. return async (dispatch) => {
  147. dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId }));
  148. dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
  149. };
  150. };
  151. /**
  152. * Initialize query history pane. To load history it requires settings to be loaded first
  153. * (but only once per session). Filters are initialised by the tab (starred or home).
  154. */
  155. export const initRichHistory = (): ThunkResult<void> => {
  156. return async (dispatch, getState) => {
  157. const queriesMigrated = store.getBool(RICH_HISTORY_SETTING_KEYS.migrated, false);
  158. const migrationFailedDuringThisSession = getState().explore.richHistoryMigrationFailed;
  159. // Query history migration should always be successful, but in case of unexpected errors we ensure
  160. // the migration attempt happens only once per session, and the user is informed about the failure
  161. // in a way that can help with potential investigation.
  162. if (config.queryHistoryEnabled && !queriesMigrated && !migrationFailedDuringThisSession) {
  163. const migrationResult = await migrateQueryHistoryFromLocalStorage();
  164. if (migrationResult.status === LocalStorageMigrationStatus.Failed) {
  165. dispatch(richHistoryMigrationFailedAction());
  166. logError(migrationResult.error!, { explore: { event: 'QueryHistoryMigrationFailed' } });
  167. } else {
  168. store.set(RICH_HISTORY_SETTING_KEYS.migrated, true);
  169. }
  170. }
  171. let settings = getState().explore.richHistorySettings;
  172. if (!settings) {
  173. settings = await getRichHistorySettings();
  174. dispatch(richHistorySettingsUpdatedAction(settings));
  175. }
  176. };
  177. };
  178. export const updateHistorySettings = (settings: RichHistorySettings): ThunkResult<void> => {
  179. return async (dispatch) => {
  180. dispatch(richHistorySettingsUpdatedAction(settings));
  181. await updateRichHistorySettings(settings);
  182. };
  183. };
  184. /**
  185. * Assumed this can be called only when settings and filters are initialised
  186. */
  187. export const updateHistorySearchFilters = (
  188. exploreId: ExploreId,
  189. filters: RichHistorySearchFilters
  190. ): ThunkResult<void> => {
  191. return async (dispatch, getState) => {
  192. await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
  193. const currentSettings = getState().explore.richHistorySettings!;
  194. if (supportedFeatures().lastUsedDataSourcesAvailable) {
  195. await dispatch(
  196. updateHistorySettings({
  197. ...currentSettings,
  198. lastUsedDatasourceFilters: filters.datasourceFilters,
  199. })
  200. );
  201. }
  202. };
  203. };
  204. export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => {
  205. if (historyUpdatedAction.match(action)) {
  206. return {
  207. ...state,
  208. history: action.payload.history,
  209. };
  210. }
  211. return state;
  212. };