explorePane.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import { createAction, PayloadAction } from '@reduxjs/toolkit';
  2. import { isEqual } from 'lodash';
  3. import { AnyAction } from 'redux';
  4. import {
  5. EventBusExtended,
  6. DataQuery,
  7. ExploreUrlState,
  8. TimeRange,
  9. HistoryItem,
  10. DataSourceApi,
  11. ExplorePanelsState,
  12. PreferredVisualisationType,
  13. } from '@grafana/data';
  14. import { getDataSourceSrv } from '@grafana/runtime';
  15. import { keybindingSrv } from 'app/core/services/keybindingSrv';
  16. import {
  17. DEFAULT_RANGE,
  18. getQueryKeys,
  19. parseUrlState,
  20. ensureQueries,
  21. generateNewKeyAndAddRefIdIfMissing,
  22. getTimeRangeFromUrl,
  23. } from 'app/core/utils/explore';
  24. import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
  25. import { ThunkResult } from 'app/types';
  26. import { ExploreGraphStyle, ExploreId, ExploreItemState } from 'app/types/explore';
  27. import { datasourceReducer } from './datasource';
  28. import { historyReducer } from './history';
  29. import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction, stateSave } from './main';
  30. import { queryReducer, runQueries, setQueriesAction } from './query';
  31. import { timeReducer, updateTime } from './time';
  32. import {
  33. makeExplorePaneState,
  34. loadAndInitDatasource,
  35. createEmptyQueryResponse,
  36. getUrlStateFromPaneState,
  37. storeGraphStyle,
  38. } from './utils';
  39. // Types
  40. //
  41. // Actions and Payloads
  42. //
  43. /**
  44. * Keep track of the Explore container size, in particular the width.
  45. * The width will be used to calculate graph intervals (number of datapoints).
  46. */
  47. export interface ChangeSizePayload {
  48. exploreId: ExploreId;
  49. width: number;
  50. height: number;
  51. }
  52. export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
  53. /**
  54. * Tracks the state of explore panels that gets synced with the url.
  55. */
  56. interface ChangePanelsState {
  57. exploreId: ExploreId;
  58. panelsState: ExplorePanelsState;
  59. }
  60. const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
  61. export function changePanelState(
  62. exploreId: ExploreId,
  63. panel: PreferredVisualisationType,
  64. panelState: ExplorePanelsState[PreferredVisualisationType]
  65. ): ThunkResult<void> {
  66. return async (dispatch, getState) => {
  67. const exploreItem = getState().explore[exploreId];
  68. if (exploreItem === undefined) {
  69. return;
  70. }
  71. const { panelsState } = exploreItem;
  72. dispatch(
  73. changePanelsStateAction({
  74. exploreId,
  75. panelsState: {
  76. ...panelsState,
  77. [panel]: panelState,
  78. },
  79. })
  80. );
  81. dispatch(stateSave());
  82. };
  83. }
  84. /**
  85. * Initialize Explore state with state from the URL and the React component.
  86. * Call this only on components for with the Explore state has not been initialized.
  87. */
  88. export interface InitializeExplorePayload {
  89. exploreId: ExploreId;
  90. containerWidth: number;
  91. eventBridge: EventBusExtended;
  92. queries: DataQuery[];
  93. range: TimeRange;
  94. history: HistoryItem[];
  95. datasourceInstance?: DataSourceApi;
  96. }
  97. export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
  98. export interface SetUrlReplacedPayload {
  99. exploreId: ExploreId;
  100. }
  101. export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
  102. /**
  103. * Keep track of the Explore container size, in particular the width.
  104. * The width will be used to calculate graph intervals (number of datapoints).
  105. */
  106. export function changeSize(
  107. exploreId: ExploreId,
  108. { height, width }: { height: number; width: number }
  109. ): PayloadAction<ChangeSizePayload> {
  110. return changeSizeAction({ exploreId, height, width });
  111. }
  112. interface ChangeGraphStylePayload {
  113. exploreId: ExploreId;
  114. graphStyle: ExploreGraphStyle;
  115. }
  116. const changeGraphStyleAction = createAction<ChangeGraphStylePayload>('explore/changeGraphStyle');
  117. export function changeGraphStyle(exploreId: ExploreId, graphStyle: ExploreGraphStyle): ThunkResult<void> {
  118. return async (dispatch, getState) => {
  119. storeGraphStyle(graphStyle);
  120. dispatch(changeGraphStyleAction({ exploreId, graphStyle }));
  121. };
  122. }
  123. /**
  124. * Initialize Explore state with state from the URL and the React component.
  125. * Call this only on components for with the Explore state has not been initialized.
  126. */
  127. export function initializeExplore(
  128. exploreId: ExploreId,
  129. datasourceNameOrUid: string,
  130. queries: DataQuery[],
  131. range: TimeRange,
  132. containerWidth: number,
  133. eventBridge: EventBusExtended,
  134. panelsState?: ExplorePanelsState
  135. ): ThunkResult<void> {
  136. return async (dispatch, getState) => {
  137. const exploreDatasources = getDataSourceSrv().getList();
  138. let instance = undefined;
  139. let history: HistoryItem[] = [];
  140. if (exploreDatasources.length >= 1) {
  141. const orgId = getState().user.orgId;
  142. const loadResult = await loadAndInitDatasource(orgId, datasourceNameOrUid);
  143. instance = loadResult.instance;
  144. history = loadResult.history;
  145. }
  146. dispatch(
  147. initializeExploreAction({
  148. exploreId,
  149. containerWidth,
  150. eventBridge,
  151. queries,
  152. range,
  153. datasourceInstance: instance,
  154. history,
  155. })
  156. );
  157. if (panelsState !== undefined) {
  158. dispatch(changePanelsStateAction({ exploreId, panelsState }));
  159. }
  160. dispatch(updateTime({ exploreId }));
  161. keybindingSrv.setupTimeRangeBindings(false);
  162. if (instance) {
  163. // We do not want to add the url to browser history on init because when the pane is initialised it's because
  164. // we already have something in the url. Adding basically the same state as additional history item prevents
  165. // user to go back to previous url.
  166. dispatch(runQueries(exploreId, { replaceUrl: true }));
  167. }
  168. };
  169. }
  170. /**
  171. * Reacts to changes in URL state that we need to sync back to our redux state. Computes diff of newUrlQuery vs current
  172. * state and runs update actions for relevant parts.
  173. */
  174. export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult<void> {
  175. return async (dispatch, getState) => {
  176. const itemState = getState().explore[exploreId]!;
  177. if (!itemState.initialized) {
  178. return;
  179. }
  180. // Get diff of what should be updated
  181. const newUrlState = parseUrlState(newUrlQuery);
  182. const update = urlDiff(newUrlState, getUrlStateFromPaneState(itemState));
  183. const { containerWidth, eventBridge } = itemState;
  184. const { datasource, queries, range: urlRange, panelsState } = newUrlState;
  185. const refreshQueries: DataQuery[] = [];
  186. for (let index = 0; index < queries.length; index++) {
  187. const query = queries[index];
  188. refreshQueries.push(generateNewKeyAndAddRefIdIfMissing(query, refreshQueries, index));
  189. }
  190. const timeZone = getTimeZone(getState().user);
  191. const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
  192. const range = getTimeRangeFromUrl(urlRange, timeZone, fiscalYearStartMonth);
  193. // commit changes based on the diff of new url vs old url
  194. if (update.datasource) {
  195. const initialQueries = ensureQueries(queries);
  196. await dispatch(
  197. initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState)
  198. );
  199. return;
  200. }
  201. if (update.range) {
  202. dispatch(updateTime({ exploreId, rawRange: range.raw }));
  203. }
  204. if (update.queries) {
  205. dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
  206. }
  207. if (update.panelsState && panelsState !== undefined) {
  208. dispatch(changePanelsStateAction({ exploreId, panelsState }));
  209. }
  210. // always run queries when refresh is needed
  211. if (update.queries || update.range) {
  212. dispatch(runQueries(exploreId));
  213. }
  214. };
  215. }
  216. /**
  217. * Reducer for an Explore area, to be used by the global Explore reducer.
  218. */
  219. // Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
  220. // ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
  221. // because the state would become frozen and during run time we would get errors because flot (Graph lib) would try to mutate
  222. // the frozen state.
  223. // https://github.com/reduxjs/redux-toolkit/issues/242
  224. export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), action: AnyAction): ExploreItemState => {
  225. state = queryReducer(state, action);
  226. state = datasourceReducer(state, action);
  227. state = timeReducer(state, action);
  228. state = historyReducer(state, action);
  229. if (richHistoryUpdatedAction.match(action)) {
  230. const { richHistory, total } = action.payload.richHistoryResults;
  231. return {
  232. ...state,
  233. richHistory,
  234. richHistoryTotal: total,
  235. };
  236. }
  237. if (richHistorySearchFiltersUpdatedAction.match(action)) {
  238. const richHistorySearchFilters = action.payload.filters;
  239. return {
  240. ...state,
  241. richHistorySearchFilters,
  242. };
  243. }
  244. if (changeSizeAction.match(action)) {
  245. const containerWidth = action.payload.width;
  246. return { ...state, containerWidth };
  247. }
  248. if (changeGraphStyleAction.match(action)) {
  249. const { graphStyle } = action.payload;
  250. return { ...state, graphStyle };
  251. }
  252. if (changePanelsStateAction.match(action)) {
  253. const { panelsState } = action.payload;
  254. return { ...state, panelsState };
  255. }
  256. if (initializeExploreAction.match(action)) {
  257. const { containerWidth, eventBridge, queries, range, datasourceInstance, history } = action.payload;
  258. return {
  259. ...state,
  260. containerWidth,
  261. eventBridge,
  262. range,
  263. queries,
  264. initialized: true,
  265. queryKeys: getQueryKeys(queries, datasourceInstance),
  266. datasourceInstance,
  267. history,
  268. datasourceMissing: !datasourceInstance,
  269. queryResponse: createEmptyQueryResponse(),
  270. cache: [],
  271. };
  272. }
  273. return state;
  274. };
  275. /**
  276. * Compare 2 explore urls and return a map of what changed. Used to update the local state with all the
  277. * side effects needed.
  278. */
  279. export const urlDiff = (
  280. oldUrlState: ExploreUrlState | undefined,
  281. currentUrlState: ExploreUrlState | undefined
  282. ): {
  283. datasource: boolean;
  284. queries: boolean;
  285. range: boolean;
  286. panelsState: boolean;
  287. } => {
  288. const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource);
  289. const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries);
  290. const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE);
  291. const panelsState = !isEqual(currentUrlState?.panelsState, oldUrlState?.panelsState);
  292. return {
  293. datasource,
  294. queries,
  295. range,
  296. panelsState,
  297. };
  298. };