Explore: Refactor & centralize URL/state sync (#66286)

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Giordano Ricci
2023-06-06 15:31:39 +01:00
committed by GitHub
parent 6900336f09
commit 067bbcbe56
42 changed files with 1360 additions and 1441 deletions

View File

@@ -1,9 +1,9 @@
import { createAction } from '@reduxjs/toolkit';
import { AnyAction } from 'redux';
import { ExploreUrlState, serializeStateToUrlParam, SplitOpenOptions, UrlQueryMap } from '@grafana/data';
import { SplitOpenOptions } from '@grafana/data';
import { DataSourceSrv, locationService } from '@grafana/runtime';
import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
import { GetExploreUrlArguments } from 'app/core/utils/explore';
import { PanelModel } from 'app/features/dashboard/state';
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
@@ -12,9 +12,10 @@ import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/uti
import { createAsyncThunk, ThunkResult } from '../../../types';
import { CorrelationData } from '../../correlations/useCorrelations';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { withUniqueRefIds } from '../utils/queries';
import { initializeExplore, paneReducer } from './explorePane';
import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
import { DEFAULT_RANGE, makeExplorePaneState } from './utils';
//
// Actions and Payloads
@@ -50,90 +51,43 @@ export const maximizePaneAction = createAction<{
export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction');
/**
* Resets state for explore.
* Close the pane with the given id.
*/
export const resetExploreAction = createAction('explore/resetExplore');
type SplitCloseActionPayload = ExploreId;
export const splitClose = createAction<SplitCloseActionPayload>('explore/splitClose');
/**
* Close the split view and save URL state.
*/
export interface SplitCloseActionPayload {
itemId: ExploreId;
export interface SetPaneStateActionPayload {
[itemId: string]: Partial<ExploreItemState>;
}
export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/splitClose');
export const setPaneState = createAction<SetPaneStateActionPayload>('explore/setPaneState');
//
// Action creators
//
export const clearPanes = createAction('explore/clearPanes');
/**
* Save local redux state back to the URL. Should be called when there is some change that should affect the URL.
* Not all of the redux state is reflected in URL though.
*/
export const stateSave = (options?: { replace?: boolean }): ThunkResult<void> => {
return (_, getState) => {
const { left, right } = getState().explore.panes;
const orgId = getState().user.orgId.toString();
const urlStates: { [index: string]: string | null } = { orgId };
urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left!));
if (right) {
urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right));
} else {
urlStates.right = null;
}
lastSavedUrl.right = urlStates.right;
lastSavedUrl.left = urlStates.left;
locationService.partial({ ...urlStates }, options?.replace);
};
};
// Store the url we saved last se we are not trying to update local state based on that.
export const lastSavedUrl: UrlQueryMap = {};
/**
* Opens a new right split pane by navigating to appropriate URL. It either copies existing state of the left pane
* or uses values from options arg. This does only navigation each pane is then responsible for initialization from
* the URL.
* Opens a new split pane. It either copies existing state of the left pane
* or uses values from options arg.
*
* TODO: this can be improved by better inferring fallback values.
*/
export const splitOpen = createAsyncThunk(
'explore/splitOpen',
async (options: SplitOpenOptions | undefined, { getState }) => {
const leftState: ExploreItemState = getState().explore.panes.left!;
const leftUrlState = getUrlStateFromPaneState(leftState);
let rightUrlState: ExploreUrlState = leftUrlState;
async (options: SplitOpenOptions | undefined, { getState, dispatch }) => {
const leftState = getState().explore.panes.left;
if (options) {
const { query, queries } = options;
const queries = options?.queries ?? (options?.query ? [options?.query] : leftState?.queries || []);
rightUrlState = {
datasource: options.datasourceUid,
queries: queries ?? (query ? [query] : []),
range: options.range || leftState.range,
panelsState: options.panelsState,
};
}
const urlState = serializeStateToUrlParam(rightUrlState);
locationService.partial({ right: urlState }, true);
await dispatch(
initializeExplore({
exploreId: ExploreId.right,
datasource: options?.datasourceUid || leftState?.datasourceInstance?.getRef(),
queries: withUniqueRefIds(queries),
range: options?.range || leftState?.range.raw || DEFAULT_RANGE,
panelsState: options?.panelsState || leftState?.panelsState,
})
);
}
);
/**
* Close the split view and save URL state. We need to update the state here because when closing we cannot just
* update the URL and let the components handle it because if we swap panes from right to left it is not easily apparent
* from the URL.
*/
export function splitClose(itemId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch(splitCloseAction({ itemId }));
dispatch(stateSave());
};
}
export interface NavigateToExploreDependencies {
getDataSourceSrv: () => DataSourceSrv;
getTimeSrv: () => TimeSrv;
@@ -169,9 +123,7 @@ export const navigateToExplore = (
const initialExploreItemState = makeExplorePaneState();
export const initialExploreState: ExploreState = {
syncedTimes: false,
panes: {
[ExploreId.left]: initialExploreItemState,
},
panes: {},
correlations: undefined,
richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false,
@@ -185,10 +137,9 @@ export const initialExploreState: ExploreState = {
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
*/
export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
if (splitCloseAction.match(action)) {
const { itemId } = action.payload;
if (splitClose.match(action)) {
const panes = {
left: itemId === ExploreId.left ? state.panes.right : state.panes.left,
left: action.payload === ExploreId.left ? state.panes.right : state.panes.left,
};
return {
...state,
@@ -254,23 +205,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (resetExploreAction.match(action)) {
// FIXME: reducers should REALLY not have side effects.
for (const [, pane] of Object.entries(state.panes).filter(([exploreId]) => exploreId !== ExploreId.left)) {
stopQueryState(pane!.querySubscription);
}
return {
...initialExploreState,
panes: {
left: {
...initialExploreItemState,
queries: state.panes.left!.queries,
},
},
};
}
if (richHistorySettingsUpdatedAction.match(action)) {
const richHistorySettings = action.payload;
return {
@@ -299,21 +233,26 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (action.payload) {
const { exploreId } = action.payload;
if (exploreId !== undefined) {
return {
...state,
panes: Object.entries(state.panes).reduce<ExploreState['panes']>((acc, [id, pane]) => {
if (id === exploreId) {
acc[id as ExploreId] = paneReducer(pane, action);
} else {
acc[id as ExploreId] = pane;
}
return acc;
}, {}),
};
}
if (clearPanes.match(action)) {
return {
...state,
panes: {},
};
}
const exploreId: ExploreId | undefined = action.payload?.exploreId;
if (typeof exploreId === 'string') {
return {
...state,
panes: Object.entries(state.panes).reduce<ExploreState['panes']>((acc, [id, pane]) => {
if (id === exploreId) {
acc[id] = paneReducer(pane, action);
} else {
acc[id as ExploreId] = pane;
}
return acc;
}, {}),
};
}
return state;