123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import { createAction, PayloadAction } from '@reduxjs/toolkit';
- import { isEqual } from 'lodash';
- import { AnyAction } from 'redux';
- import {
- EventBusExtended,
- DataQuery,
- ExploreUrlState,
- TimeRange,
- HistoryItem,
- DataSourceApi,
- ExplorePanelsState,
- PreferredVisualisationType,
- } from '@grafana/data';
- import { getDataSourceSrv } from '@grafana/runtime';
- import { keybindingSrv } from 'app/core/services/keybindingSrv';
- import {
- DEFAULT_RANGE,
- getQueryKeys,
- parseUrlState,
- ensureQueries,
- generateNewKeyAndAddRefIdIfMissing,
- getTimeRangeFromUrl,
- } from 'app/core/utils/explore';
- import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
- import { ThunkResult } from 'app/types';
- import { ExploreGraphStyle, ExploreId, ExploreItemState } from 'app/types/explore';
- import { datasourceReducer } from './datasource';
- import { historyReducer } from './history';
- import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction, stateSave } from './main';
- import { queryReducer, runQueries, setQueriesAction } from './query';
- import { timeReducer, updateTime } from './time';
- import {
- makeExplorePaneState,
- loadAndInitDatasource,
- createEmptyQueryResponse,
- getUrlStateFromPaneState,
- storeGraphStyle,
- } from './utils';
- // Types
- //
- // Actions and Payloads
- //
- /**
- * Keep track of the Explore container size, in particular the width.
- * The width will be used to calculate graph intervals (number of datapoints).
- */
- export interface ChangeSizePayload {
- exploreId: ExploreId;
- width: number;
- height: number;
- }
- export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
- /**
- * Tracks the state of explore panels that gets synced with the url.
- */
- interface ChangePanelsState {
- exploreId: ExploreId;
- panelsState: ExplorePanelsState;
- }
- const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
- export function changePanelState(
- exploreId: ExploreId,
- panel: PreferredVisualisationType,
- panelState: ExplorePanelsState[PreferredVisualisationType]
- ): ThunkResult<void> {
- return async (dispatch, getState) => {
- const exploreItem = getState().explore[exploreId];
- if (exploreItem === undefined) {
- return;
- }
- const { panelsState } = exploreItem;
- dispatch(
- changePanelsStateAction({
- exploreId,
- panelsState: {
- ...panelsState,
- [panel]: panelState,
- },
- })
- );
- dispatch(stateSave());
- };
- }
- /**
- * Initialize Explore state with state from the URL and the React component.
- * Call this only on components for with the Explore state has not been initialized.
- */
- export interface InitializeExplorePayload {
- exploreId: ExploreId;
- containerWidth: number;
- eventBridge: EventBusExtended;
- queries: DataQuery[];
- range: TimeRange;
- history: HistoryItem[];
- datasourceInstance?: DataSourceApi;
- }
- export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
- export interface SetUrlReplacedPayload {
- exploreId: ExploreId;
- }
- export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
- /**
- * Keep track of the Explore container size, in particular the width.
- * The width will be used to calculate graph intervals (number of datapoints).
- */
- export function changeSize(
- exploreId: ExploreId,
- { height, width }: { height: number; width: number }
- ): PayloadAction<ChangeSizePayload> {
- return changeSizeAction({ exploreId, height, width });
- }
- interface ChangeGraphStylePayload {
- exploreId: ExploreId;
- graphStyle: ExploreGraphStyle;
- }
- const changeGraphStyleAction = createAction<ChangeGraphStylePayload>('explore/changeGraphStyle');
- export function changeGraphStyle(exploreId: ExploreId, graphStyle: ExploreGraphStyle): ThunkResult<void> {
- return async (dispatch, getState) => {
- storeGraphStyle(graphStyle);
- dispatch(changeGraphStyleAction({ exploreId, graphStyle }));
- };
- }
- /**
- * Initialize Explore state with state from the URL and the React component.
- * Call this only on components for with the Explore state has not been initialized.
- */
- export function initializeExplore(
- exploreId: ExploreId,
- datasourceNameOrUid: string,
- queries: DataQuery[],
- range: TimeRange,
- containerWidth: number,
- eventBridge: EventBusExtended,
- panelsState?: ExplorePanelsState
- ): ThunkResult<void> {
- return async (dispatch, getState) => {
- const exploreDatasources = getDataSourceSrv().getList();
- let instance = undefined;
- let history: HistoryItem[] = [];
- if (exploreDatasources.length >= 1) {
- const orgId = getState().user.orgId;
- const loadResult = await loadAndInitDatasource(orgId, datasourceNameOrUid);
- instance = loadResult.instance;
- history = loadResult.history;
- }
- dispatch(
- initializeExploreAction({
- exploreId,
- containerWidth,
- eventBridge,
- queries,
- range,
- datasourceInstance: instance,
- history,
- })
- );
- if (panelsState !== undefined) {
- dispatch(changePanelsStateAction({ exploreId, panelsState }));
- }
- dispatch(updateTime({ exploreId }));
- keybindingSrv.setupTimeRangeBindings(false);
- if (instance) {
- // We do not want to add the url to browser history on init because when the pane is initialised it's because
- // we already have something in the url. Adding basically the same state as additional history item prevents
- // user to go back to previous url.
- dispatch(runQueries(exploreId, { replaceUrl: true }));
- }
- };
- }
- /**
- * Reacts to changes in URL state that we need to sync back to our redux state. Computes diff of newUrlQuery vs current
- * state and runs update actions for relevant parts.
- */
- export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult<void> {
- return async (dispatch, getState) => {
- const itemState = getState().explore[exploreId]!;
- if (!itemState.initialized) {
- return;
- }
- // Get diff of what should be updated
- const newUrlState = parseUrlState(newUrlQuery);
- const update = urlDiff(newUrlState, getUrlStateFromPaneState(itemState));
- const { containerWidth, eventBridge } = itemState;
- const { datasource, queries, range: urlRange, panelsState } = newUrlState;
- const refreshQueries: DataQuery[] = [];
- for (let index = 0; index < queries.length; index++) {
- const query = queries[index];
- refreshQueries.push(generateNewKeyAndAddRefIdIfMissing(query, refreshQueries, index));
- }
- const timeZone = getTimeZone(getState().user);
- const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
- const range = getTimeRangeFromUrl(urlRange, timeZone, fiscalYearStartMonth);
- // commit changes based on the diff of new url vs old url
- if (update.datasource) {
- const initialQueries = ensureQueries(queries);
- await dispatch(
- initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState)
- );
- return;
- }
- if (update.range) {
- dispatch(updateTime({ exploreId, rawRange: range.raw }));
- }
- if (update.queries) {
- dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
- }
- if (update.panelsState && panelsState !== undefined) {
- dispatch(changePanelsStateAction({ exploreId, panelsState }));
- }
- // always run queries when refresh is needed
- if (update.queries || update.range) {
- dispatch(runQueries(exploreId));
- }
- };
- }
- /**
- * Reducer for an Explore area, to be used by the global Explore reducer.
- */
- // Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
- // ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
- // because the state would become frozen and during run time we would get errors because flot (Graph lib) would try to mutate
- // the frozen state.
- // https://github.com/reduxjs/redux-toolkit/issues/242
- export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), action: AnyAction): ExploreItemState => {
- 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;
- return {
- ...state,
- richHistory,
- richHistoryTotal: total,
- };
- }
- if (richHistorySearchFiltersUpdatedAction.match(action)) {
- const richHistorySearchFilters = action.payload.filters;
- return {
- ...state,
- richHistorySearchFilters,
- };
- }
- if (changeSizeAction.match(action)) {
- const containerWidth = action.payload.width;
- return { ...state, containerWidth };
- }
- if (changeGraphStyleAction.match(action)) {
- const { graphStyle } = action.payload;
- return { ...state, graphStyle };
- }
- if (changePanelsStateAction.match(action)) {
- const { panelsState } = action.payload;
- return { ...state, panelsState };
- }
- if (initializeExploreAction.match(action)) {
- const { containerWidth, eventBridge, queries, range, datasourceInstance, history } = action.payload;
- return {
- ...state,
- containerWidth,
- eventBridge,
- range,
- queries,
- initialized: true,
- queryKeys: getQueryKeys(queries, datasourceInstance),
- datasourceInstance,
- history,
- datasourceMissing: !datasourceInstance,
- queryResponse: createEmptyQueryResponse(),
- cache: [],
- };
- }
- return state;
- };
- /**
- * Compare 2 explore urls and return a map of what changed. Used to update the local state with all the
- * side effects needed.
- */
- export const urlDiff = (
- oldUrlState: ExploreUrlState | undefined,
- currentUrlState: ExploreUrlState | undefined
- ): {
- datasource: boolean;
- queries: boolean;
- range: boolean;
- panelsState: boolean;
- } => {
- const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource);
- const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries);
- const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE);
- const panelsState = !isEqual(currentUrlState?.panelsState, oldUrlState?.panelsState);
- return {
- datasource,
- queries,
- range,
- panelsState,
- };
- };
|