main.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import { createAction } from '@reduxjs/toolkit';
  2. import { AnyAction } from 'redux';
  3. import { ExploreUrlState, serializeStateToUrlParam, SplitOpen, UrlQueryMap } from '@grafana/data';
  4. import { DataSourceSrv, getDataSourceSrv, locationService } from '@grafana/runtime';
  5. import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
  6. import { PanelModel } from 'app/features/dashboard/state';
  7. import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
  8. import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
  9. import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
  10. import { ThunkResult } from '../../../types';
  11. import { TimeSrv } from '../../dashboard/services/TimeSrv';
  12. import { paneReducer } from './explorePane';
  13. import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
  14. //
  15. // Actions and Payloads
  16. //
  17. export interface SyncTimesPayload {
  18. syncedTimes: boolean;
  19. }
  20. export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
  21. export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: ExploreId }>(
  22. 'explore/richHistoryUpdated'
  23. );
  24. export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
  25. export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction');
  26. export const richHistoryMigrationFailedAction = createAction('explore/richHistoryMigrationFailedAction');
  27. export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
  28. export const richHistorySearchFiltersUpdatedAction = createAction<{
  29. exploreId: ExploreId;
  30. filters?: RichHistorySearchFilters;
  31. }>('explore/richHistorySearchFiltersUpdatedAction');
  32. /**
  33. * Resets state for explore.
  34. */
  35. export interface ResetExplorePayload {
  36. force?: boolean;
  37. }
  38. export const resetExploreAction = createAction<ResetExplorePayload>('explore/resetExplore');
  39. /**
  40. * Close the split view and save URL state.
  41. */
  42. export interface SplitCloseActionPayload {
  43. itemId: ExploreId;
  44. }
  45. export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/splitClose');
  46. /**
  47. * Cleans up a pane state. Could seem like this should be in explorePane.ts actions but in case we area closing
  48. * left pane we need to move right state to the left.
  49. * Also this may seem redundant as we have splitClose actions which clears up state but that action is not called on
  50. * URL change.
  51. */
  52. export interface CleanupPanePayload {
  53. exploreId: ExploreId;
  54. }
  55. export const cleanupPaneAction = createAction<CleanupPanePayload>('explore/cleanupPane');
  56. //
  57. // Action creators
  58. //
  59. /**
  60. * Save local redux state back to the URL. Should be called when there is some change that should affect the URL.
  61. * Not all of the redux state is reflected in URL though.
  62. */
  63. export const stateSave = (options?: { replace?: boolean }): ThunkResult<void> => {
  64. return (dispatch, getState) => {
  65. const { left, right } = getState().explore;
  66. const orgId = getState().user.orgId.toString();
  67. const urlStates: { [index: string]: string | null } = { orgId };
  68. urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left));
  69. if (right) {
  70. urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right));
  71. } else {
  72. urlStates.right = null;
  73. }
  74. lastSavedUrl.right = urlStates.right;
  75. lastSavedUrl.left = urlStates.left;
  76. locationService.partial({ ...urlStates }, options?.replace);
  77. };
  78. };
  79. // Store the url we saved last se we are not trying to update local state based on that.
  80. export const lastSavedUrl: UrlQueryMap = {};
  81. /**
  82. * Opens a new right split pane by navigating to appropriate URL. It either copies existing state of the left pane
  83. * or uses values from options arg. This does only navigation each pane is then responsible for initialization from
  84. * the URL.
  85. */
  86. export const splitOpen: SplitOpen = (options): ThunkResult<void> => {
  87. return async (dispatch, getState) => {
  88. const leftState: ExploreItemState = getState().explore[ExploreId.left];
  89. const leftUrlState = getUrlStateFromPaneState(leftState);
  90. let rightUrlState: ExploreUrlState = leftUrlState;
  91. if (options) {
  92. const datasourceName = getDataSourceSrv().getInstanceSettings(options.datasourceUid)?.name || '';
  93. rightUrlState = {
  94. datasource: datasourceName,
  95. queries: [options.query],
  96. range: options.range || leftState.range,
  97. panelsState: options.panelsState,
  98. };
  99. }
  100. const urlState = serializeStateToUrlParam(rightUrlState);
  101. locationService.partial({ right: urlState }, true);
  102. };
  103. };
  104. /**
  105. * Close the split view and save URL state. We need to update the state here because when closing we cannot just
  106. * update the URL and let the components handle it because if we swap panes from right to left it is not easily apparent
  107. * from the URL.
  108. */
  109. export function splitClose(itemId: ExploreId): ThunkResult<void> {
  110. return (dispatch, getState) => {
  111. dispatch(splitCloseAction({ itemId }));
  112. dispatch(stateSave());
  113. };
  114. }
  115. export interface NavigateToExploreDependencies {
  116. getDataSourceSrv: () => DataSourceSrv;
  117. getTimeSrv: () => TimeSrv;
  118. getExploreUrl: (args: GetExploreUrlArguments) => Promise<string | undefined>;
  119. openInNewWindow?: (url: string) => void;
  120. }
  121. export const navigateToExplore = (
  122. panel: PanelModel,
  123. dependencies: NavigateToExploreDependencies
  124. ): ThunkResult<void> => {
  125. return async (dispatch) => {
  126. const { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow } = dependencies;
  127. const datasourceSrv = getDataSourceSrv();
  128. const path = await getExploreUrl({
  129. panel,
  130. datasourceSrv,
  131. timeSrv: getTimeSrv(),
  132. });
  133. if (openInNewWindow && path) {
  134. openInNewWindow(path);
  135. return;
  136. }
  137. locationService.push(path!);
  138. };
  139. };
  140. /**
  141. * Global Explore state that handles multiple Explore areas and the split state
  142. */
  143. const initialExploreItemState = makeExplorePaneState();
  144. export const initialExploreState: ExploreState = {
  145. syncedTimes: false,
  146. left: initialExploreItemState,
  147. right: undefined,
  148. richHistoryStorageFull: false,
  149. richHistoryLimitExceededWarningShown: false,
  150. richHistoryMigrationFailed: false,
  151. };
  152. /**
  153. * Global Explore reducer that handles multiple Explore areas (left and right).
  154. * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  155. */
  156. export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
  157. if (splitCloseAction.match(action)) {
  158. const { itemId } = action.payload as SplitCloseActionPayload;
  159. const targetSplit = {
  160. left: itemId === ExploreId.left ? state.right! : state.left,
  161. right: undefined,
  162. };
  163. return {
  164. ...state,
  165. ...targetSplit,
  166. };
  167. }
  168. if (cleanupPaneAction.match(action)) {
  169. const { exploreId } = action.payload as CleanupPanePayload;
  170. // We want to do this only when we remove single pane not when we are unmounting whole explore.
  171. // It needs to be checked like this because in component we don't get new path (which would tell us if we are
  172. // navigating out of explore) before the unmount.
  173. if (!state[exploreId]?.initialized) {
  174. return state;
  175. }
  176. if (exploreId === ExploreId.left) {
  177. return {
  178. ...state,
  179. [ExploreId.left]: state[ExploreId.right]!,
  180. [ExploreId.right]: undefined,
  181. };
  182. } else {
  183. return {
  184. ...state,
  185. [ExploreId.right]: undefined,
  186. };
  187. }
  188. }
  189. if (syncTimesAction.match(action)) {
  190. return { ...state, syncedTimes: action.payload.syncedTimes };
  191. }
  192. if (richHistoryStorageFullAction.match(action)) {
  193. return {
  194. ...state,
  195. richHistoryStorageFull: true,
  196. };
  197. }
  198. if (richHistoryLimitExceededAction.match(action)) {
  199. return {
  200. ...state,
  201. richHistoryLimitExceededWarningShown: true,
  202. };
  203. }
  204. if (richHistoryMigrationFailedAction.match(action)) {
  205. return {
  206. ...state,
  207. richHistoryMigrationFailed: true,
  208. };
  209. }
  210. if (resetExploreAction.match(action)) {
  211. const payload: ResetExplorePayload = action.payload;
  212. const leftState = state[ExploreId.left];
  213. const rightState = state[ExploreId.right];
  214. stopQueryState(leftState.querySubscription);
  215. if (rightState) {
  216. stopQueryState(rightState.querySubscription);
  217. }
  218. if (payload.force) {
  219. return initialExploreState;
  220. }
  221. return {
  222. ...initialExploreState,
  223. left: {
  224. ...initialExploreItemState,
  225. queries: state.left.queries,
  226. },
  227. };
  228. }
  229. if (richHistorySettingsUpdatedAction.match(action)) {
  230. const richHistorySettings = action.payload;
  231. return {
  232. ...state,
  233. richHistorySettings,
  234. };
  235. }
  236. if (action.payload) {
  237. const { exploreId } = action.payload;
  238. if (exploreId !== undefined) {
  239. // @ts-ignore
  240. const explorePaneState = state[exploreId];
  241. return { ...state, [exploreId]: paneReducer(explorePaneState, action as any) };
  242. }
  243. }
  244. return state;
  245. };
  246. export default {
  247. explore: exploreReducer,
  248. };