Explore: move panes into a keyed object (#66117)

This commit is contained in:
Giordano Ricci 2023-05-03 16:45:11 +01:00 committed by GitHub
parent 049029b6a2
commit f03d0698d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 501 additions and 409 deletions

View File

@ -385,8 +385,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
],
"packages/grafana-data/src/types/explore.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-data/src/types/fieldOverrides.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -2748,9 +2747,25 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/explore/state/history.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/state/main.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/explore/state/query.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/state/time.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/explore/state/time.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
],
"public/app/features/explore/state/utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -22,7 +22,7 @@ export interface ExploreTracePanelState {
spanId?: string;
}
export interface SplitOpenOptions<T> {
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
datasourceUid: string;
/** @deprecated Will be removed in a future version. Use queries instead. */
query?: T;
@ -34,4 +34,4 @@ export interface SplitOpenOptions<T> {
/**
* SplitOpen type is used in Explore and related components.
*/
export type SplitOpen = <T extends DataQuery = any>(options?: SplitOpenOptions<T> | undefined) => void;
export type SplitOpen = (options?: SplitOpenOptions | undefined) => void;

View File

@ -2,9 +2,9 @@ import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { RefreshPicker } from '@grafana/ui';
import store from 'app/core/store';
import { ExploreId } from 'app/types';
import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv';
import { ExploreId } from '../../types';
import {
buildQueryTransaction,

View File

@ -22,11 +22,13 @@ import { AddToDashboard } from '.';
const setup = (children: ReactNode, queries: DataQuery[] = [{ refId: 'A' }]) => {
const store = configureStore({
explore: {
left: {
queries,
queryResponse: createEmptyQueryResponse(),
panes: {
left: {
queries,
queryResponse: createEmptyQueryResponse(),
},
},
} as ExploreState,
} as unknown as ExploreState,
});
return render(<Provider store={store}>{children}</Provider>);

View File

@ -4,7 +4,7 @@ import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { TestProvider } from 'test/helpers/TestProvider';
import { DataSourceApi, LoadingState, CoreApp, createTheme, EventBusSrv } from '@grafana/data';
import { ExploreId } from 'app/types/explore';
import { ExploreId } from 'app/types';
import { Explore, Props } from './Explore';
import { scanStopAction } from './state/query';
@ -86,7 +86,7 @@ const dummyProps: Props = {
showTrace: true,
showNodeGraph: true,
showFlameGraph: true,
splitOpen: () => {},
splitOpen: jest.fn(),
splitted: false,
isFromCompactUrl: false,
eventBus: new EventBusSrv(),

View File

@ -253,7 +253,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
};
onSplitOpen = (panelType: string) => {
return async (options?: SplitOpenOptions<DataQuery>) => {
return async (options?: SplitOpenOptions) => {
this.props.splitOpen(options);
if (options && this.props.datasourceInstance) {
const target = (await getDataSourceSrv().get(options.datasourceUid)).type;
@ -546,7 +546,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
const explore = state.explore;
const { syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId]!;
const item: ExploreItemState = explore.panes[exploreId]!;
const timeZone = getTimeZone(state.user);
const {
datasourceInstance,

View File

@ -168,7 +168,9 @@ export function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
const useExplorePageTitle = () => {
const navModel = useNavModel('explore');
const datasources = useSelector((state) =>
[state.explore.left.datasourceInstance?.name, state.explore.right?.datasourceInstance?.name].filter(isTruthy)
[state.explore.panes.left!.datasourceInstance?.name, state.explore.panes.right?.datasourceInstance?.name].filter(
isTruthy
)
);
document.title = `${navModel.main.text} - ${datasources.join(' | ')} - ${Branding.AppTitle}`;

View File

@ -122,16 +122,16 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
reportInteraction('grafana_explore_compact_notice');
}
this.props.initializeExplore(
this.props.initializeExplore({
exploreId,
rootDatasourceOverride || queries[0]?.datasource || initialDatasource,
datasource: rootDatasourceOverride || queries[0]?.datasource || initialDatasource,
queries,
initialRange,
width,
this.exploreEvents,
range: initialRange,
containerWidth: width,
eventBridge: this.exploreEvents,
panelsState,
isFromCompactUrl
);
isFromCompactUrl,
});
}
}
@ -181,7 +181,7 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
: getTimeRange(timeZone, DEFAULT_RANGE, fiscalYearStartMonth);
return {
initialized: state.explore[props.exploreId]?.initialized,
initialized: state.explore.panes[props.exploreId]?.initialized,
initialDatasource,
initialQueries: queries,
initialRange,

View File

@ -90,7 +90,7 @@ export function ExploreQueryInspector(props: Props) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId]!;
const item: ExploreItemState = explore.panes[exploreId]!;
const { loading, queryResponse } = item;
return {

View File

@ -52,7 +52,7 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
const { refreshInterval, loading, datasourceInstance, range, isLive, isPaused, syncedTimes } = useSelector(
(state: StoreState) => ({
...pick(
state.explore[exploreId]!,
state.explore.panes[exploreId]!,
'refreshInterval',
'loading',
'datasourceInstance',
@ -65,9 +65,9 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
shallowEqual
);
const isLargerPane = useSelector((state: StoreState) => state.explore.largerExploreId === exploreId);
const showSmallTimePicker = useSelector((state) => splitted || state.explore[exploreId]!.containerWidth < 1210);
const showSmallTimePicker = useSelector((state) => splitted || state.explore.panes[exploreId]!.containerWidth < 1210);
const showSmallDataSourcePicker = useSelector(
(state) => state.explore[exploreId]!.containerWidth < (splitted ? 700 : 800)
(state) => state.explore.panes[exploreId]!.containerWidth < (splitted ? 700 : 800)
);
const shouldRotateSplitIcon = useMemo(

View File

@ -208,10 +208,9 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
}
}
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const item: ExploreItemState = explore.panes[exploreId]!;
const {
logsResult,
loading,

View File

@ -2,8 +2,7 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data';
import { ExploreId } from '../../types';
import { ExploreId } from 'app/types';
import { UnconnectedNodeGraphContainer } from './NodeGraphContainer';

View File

@ -108,7 +108,7 @@ export function UnconnectedNodeGraphContainer(props: Props) {
function mapStateToProps(state: StoreState, { exploreId }: OwnProps) {
return {
range: state.explore[exploreId]!.range,
range: state.explore.panes[exploreId]!.range,
};
}

View File

@ -50,15 +50,16 @@ function setup(queries: DataQuery[]) {
const leftState = makeExplorePaneState();
const initialState: ExploreState = {
left: {
...leftState,
richHistory: [],
datasourceInstance: datasources['someDs-uid'],
queries,
panes: {
left: {
...leftState,
richHistory: [],
datasourceInstance: datasources['someDs-uid'],
queries,
},
},
syncedTimes: false,
correlations: [],
right: undefined,
richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false,
};

View File

@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
import React from 'react';
import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
import { ExploreId, TABLE_RESULTS_STYLE } from 'app/types/explore';
import { ExploreId, TABLE_RESULTS_STYLE } from 'app/types';
import { RawPrometheusContainer } from './RawPrometheusContainer';

View File

@ -30,7 +30,7 @@ interface PrometheusContainerState {
function mapStateToProps(state: StoreState, { exploreId }: RawPrometheusContainerProps) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId]!;
const item: ExploreItemState = explore.panes[exploreId]!;
const { loading: loadingInState, tableResult, rawPrometheusResult, range } = item;
const rawPrometheusFrame: DataFrame[] = rawPrometheusResult ? [rawPrometheusResult] : [];
const result = (tableResult?.length ?? false) > 0 && rawPrometheusResult ? tableResult : rawPrometheusFrame;

View File

@ -4,9 +4,9 @@ import { Provider } from 'react-redux';
import { DataQueryError, LoadingState, getDefaultTimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ExploreId } from 'app/types';
import { configureStore } from '../../store/configureStore';
import { ExploreId } from '../../types';
import { ResponseErrorContainer } from './ResponseErrorContainer';
@ -46,7 +46,7 @@ describe('ResponseErrorContainer', () => {
function setup(error: DataQueryError) {
const store = configureStore();
store.getState().explore[ExploreId.left].queryResponse = {
store.getState().explore.panes.left!.queryResponse = {
timeRange: getDefaultTimeRange(),
series: [],
state: LoadingState.Error,

View File

@ -11,7 +11,7 @@ interface Props {
exploreId: ExploreId;
}
export function ResponseErrorContainer(props: Props) {
const queryResponse = useSelector((state) => state.explore[props.exploreId]?.queryResponse);
const queryResponse = useSelector((state) => state.explore.panes[props.exploreId]!.queryResponse);
const queryError = queryResponse?.state === LoadingState.Error ? queryResponse?.error : undefined;
// Errors with ref ids are shown below the corresponding query

View File

@ -3,8 +3,7 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SortOrder } from 'app/core/utils/richHistory';
import { ExploreId } from '../../../types/explore';
import { ExploreId } from 'app/types';
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';

View File

@ -6,8 +6,8 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from
import { DataQuery, DataSourceRef } from '@grafana/schema';
import appEvents from 'app/core/app_events';
import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ExploreId, RichHistoryQuery } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ExploreId, RichHistoryQuery } from 'app/types/explore';
import { RichHistoryCard, Props } from './RichHistoryCard';

View File

@ -23,7 +23,7 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const { datasourceInstance } = explore[exploreId]!;
const { datasourceInstance } = explore.panes[exploreId]!;
return {
exploreId,
datasourceInstance,

View File

@ -2,8 +2,7 @@ import { render } from '@testing-library/react';
import React from 'react';
import { SortOrder } from 'app/core/utils/richHistory';
import { ExploreId } from '../../../types/explore';
import { ExploreId } from 'app/types';
import { Tabs } from './RichHistory';
import { RichHistoryContainer, Props } from './RichHistoryContainer';

View File

@ -26,8 +26,7 @@ import { RichHistory, Tabs } from './RichHistory';
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const item: ExploreItemState = explore.panes[exploreId]!;
const richHistorySearchFilters = item.richHistorySearchFilters;
const richHistorySettings = explore.richHistorySettings;
const { datasourceInstance } = item;

View File

@ -2,8 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { SortOrder } from 'app/core/utils/richHistory';
import { ExploreId } from '../../../types/explore';
import { ExploreId } from 'app/types';
import { RichHistoryStarredTab, RichHistoryStarredTabProps } from './RichHistoryStarredTab';

View File

@ -2,7 +2,7 @@ import { render, screen, within } from '@testing-library/react';
import React from 'react';
import { DataFrame, FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
import { ExploreId } from 'app/types/explore';
import { ExploreId } from 'app/types';
import { TableContainer } from './TableContainer';

View File

@ -21,8 +21,7 @@ interface TableContainerProps {
function mapStateToProps(state: StoreState, { exploreId }: TableContainerProps) {
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const item: ExploreItemState = explore.panes[exploreId]!;
const { loading: loadingInState, tableResult, range } = item;
const loading = tableResult && tableResult.length > 0 ? false : loadingInState;
return { loading, tableResult, range };

View File

@ -256,7 +256,7 @@ function useFocusSpanLink(options: {
refId?: string;
datasource?: DataSourceApi;
}): [string | undefined, (traceId: string, spanId: string) => LinkModel<Field>] {
const panelState = useSelector((state) => state.explore[options.exploreId]?.panelsState.trace);
const panelState = useSelector((state) => state.explore.panes[options.exploreId]?.panelsState.trace);
const focusedSpanId = panelState?.spanId;
const dispatch = useDispatch();
@ -269,7 +269,7 @@ function useFocusSpanLink(options: {
);
const query = useSelector((state) =>
state.explore[options.exploreId]?.queries.find((query) => query.refId === options.refId)
state.explore.panes[options.exploreId]?.queries.find((query) => query.refId === options.refId)
);
const createFocusSpanLink = (traceId: string, spanId: string) => {

View File

@ -47,7 +47,7 @@ export function TraceViewContainer(props: Props) {
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [searchBarSuffix, setSearchBarSuffix] = useState('');
const datasource = useSelector(
(state: StoreState) => state.explore[props.exploreId!]?.datasourceInstance ?? undefined
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
);
const datasourceType = datasource ? datasource?.type : 'unknown';

View File

@ -3,9 +3,9 @@ import { of } from 'rxjs';
import { serializeStateToUrlParam } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ExploreId } from 'app/types';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import { ExploreId } from '../../../types';
import {
assertDataSourceFilterVisibility,

View File

@ -45,7 +45,7 @@ export function changeDatasource(
return async (dispatch, getState) => {
const orgId = getState().user.orgId;
const { history, instance } = await loadAndInitDatasource(orgId, { uid: datasourceUid });
const currentDataSourceInstance = getState().explore[exploreId]!.datasourceInstance;
const currentDataSourceInstance = getState().explore.panes[exploreId]!.datasourceInstance;
reportInteraction('explore_change_ds', {
from: (currentDataSourceInstance?.meta?.mixed ? 'mixed' : currentDataSourceInstance?.type) || 'unknown',
@ -61,11 +61,11 @@ export function changeDatasource(
);
if (options?.importQueries) {
const queries = getState().explore[exploreId]!.queries;
const queries = getState().explore.panes[exploreId]!.queries;
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
}
if (getState().explore[exploreId]!.isLive) {
if (getState().explore.panes[exploreId]!.isLive) {
dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value));
}

View File

@ -29,9 +29,11 @@ function setupStore(state?: any) {
return configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
...(state || {}),
panes: {
[ExploreId.left]: {
...defaultInitialState.explore.panes.left,
...(state || {}),
},
},
},
} as any);
@ -98,7 +100,7 @@ describe('refreshExplore', () => {
await dispatch(
refreshExplore(ExploreId.left, serializeStateToUrlParam({ datasource: 'newDs', queries: [], range: testRange }))
);
expect(getState().explore[ExploreId.left].datasourceInstance?.name).toBe('newDs');
expect(getState().explore.panes.left!.datasourceInstance?.name).toBe('newDs');
});
it('should change and run new queries from the URL', async () => {
@ -111,21 +113,19 @@ describe('refreshExplore', () => {
)
);
// same
const state = getState().explore[ExploreId.left];
const state = getState().explore.panes.left!;
expect(state.datasourceInstance?.name).toBe('someDs');
expect(state.queries.length).toBe(1);
expect(state.queries).toMatchObject([{ expr: 'count()' }]);
expect(datasources.someDs.query).toHaveBeenCalledTimes(1);
});
it('should not do anything if pane is not initialized', async () => {
const { dispatch, getState } = setup({
initialized: false,
});
it('should not do anything if pane is not present', async () => {
const { dispatch, getState } = setup({});
const state = getState();
await dispatch(
refreshExplore(
ExploreId.left,
ExploreId.right,
serializeStateToUrlParam({ datasource: 'newDs', queries: [{ expr: 'count()', refId: 'A' }], range: testRange })
)
);

View File

@ -22,7 +22,7 @@ import {
getTimeRangeFromUrl,
} from 'app/core/utils/explore';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { ThunkResult } from 'app/types';
import { createAsyncThunk, ThunkResult } from 'app/types';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { datasourceReducer } from './datasource';
@ -67,7 +67,7 @@ export function changePanelState(
panelState: ExplorePanelsState[PreferredVisualisationType]
): ThunkResult<void> {
return async (dispatch, getState) => {
const exploreItem = getState().explore[exploreId];
const exploreItem = getState().explore.panes[exploreId];
if (exploreItem === undefined) {
return;
}
@ -89,7 +89,7 @@ export function changePanelState(
* 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 {
interface InitializeExplorePayload {
exploreId: ExploreId;
containerWidth: number;
eventBridge: EventBusExtended;
@ -99,7 +99,7 @@ export interface InitializeExplorePayload {
datasourceInstance?: DataSourceApi;
isFromCompactUrl?: boolean;
}
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExploreAction');
export interface SetUrlReplacedPayload {
exploreId: ExploreId;
@ -117,6 +117,16 @@ export function changeSize(
return changeSizeAction({ exploreId, height, width });
}
interface InitializeExploreOptions {
exploreId: ExploreId;
datasource: DataSourceRef | string;
queries: DataQuery[];
range: TimeRange;
containerWidth: number;
eventBridge: EventBusExtended;
panelsState?: ExplorePanelsState;
isFromCompactUrl?: boolean;
}
/**
* 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.
@ -125,17 +135,21 @@ export function changeSize(
* and can be either a string that is the name or uid, or a datasourceRef
* This is to maximize compatability with how datasources are accessed from the URL param.
*/
export function initializeExplore(
exploreId: ExploreId,
datasource: DataSourceRef | string,
queries: DataQuery[],
range: TimeRange,
containerWidth: number,
eventBridge: EventBusExtended,
panelsState?: ExplorePanelsState,
isFromCompactUrl?: boolean
): ThunkResult<void> {
return async (dispatch, getState) => {
export const initializeExplore = createAsyncThunk(
'explore/initializeExplore',
async (
{
exploreId,
datasource,
queries,
range,
containerWidth,
eventBridge,
panelsState,
isFromCompactUrl,
}: InitializeExploreOptions,
{ dispatch, getState }
) => {
const exploreDatasources = getDataSourceSrv().getList();
let instance = undefined;
let history: HistoryItem[] = [];
@ -170,8 +184,8 @@ export function initializeExplore(
// 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
@ -179,8 +193,8 @@ export function initializeExplore(
*/
export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult<void> {
return async (dispatch, getState) => {
const itemState = getState().explore[exploreId];
if (!itemState?.initialized) {
const itemState = getState().explore.panes[exploreId];
if (!itemState) {
return;
}
@ -208,7 +222,15 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
if (update.datasource) {
const initialQueries = await ensureQueries(queries);
await dispatch(
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState)
initializeExplore({
exploreId,
datasource,
queries: initialQueries,
range,
containerWidth,
eventBridge,
panelsState,
})
);
return;
}

View File

@ -1,7 +1,5 @@
import { DefaultTimeZone, TimeRange, toUtc, SupplementaryQueryType } from '@grafana/data';
import { ExploreId } from '../../../types';
export const createDefaultInitialState = () => {
const t = toUtc();
const testRange: TimeRange = {
@ -19,37 +17,39 @@ export const createDefaultInitialState = () => {
timeZone: DefaultTimeZone,
},
explore: {
[ExploreId.left]: {
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
getDataProvider: jest.fn(),
getSupportedSupplementaryQueryTypes: jest
.fn()
.mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]),
getSupplementaryQuery: jest.fn(),
meta: {
id: 'something',
panes: {
left: {
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
getDataProvider: jest.fn(),
getSupportedSupplementaryQueryTypes: jest
.fn()
.mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]),
getSupplementaryQuery: jest.fn(),
meta: {
id: 'something',
},
},
},
initialized: true,
containerWidth: 1920,
eventBridge: { emit: () => {} },
queries: [{ expr: 'test' }],
range: testRange,
history: [],
refreshInterval: {
label: 'Off',
value: 0,
},
cache: [],
richHistory: [],
supplementaryQueries: {
[SupplementaryQueryType.LogsVolume]: {
enabled: true,
initialized: true,
containerWidth: 1920,
eventBridge: { emit: () => {} },
queries: [{ expr: 'test' }],
range: testRange,
history: [],
refreshInterval: {
label: 'Off',
value: 0,
},
[SupplementaryQueryType.LogsSample]: {
enabled: true,
cache: [],
richHistory: [],
supplementaryQueries: {
[SupplementaryQueryType.LogsVolume]: {
enabled: true,
},
[SupplementaryQueryType.LogsSample]: {
enabled: true,
},
},
},
},

View File

@ -67,8 +67,9 @@ const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesO
};
const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: ExploreId) => void) => {
callback(state.left, ExploreId.left);
state.right && callback(state.right, ExploreId.right);
Object.entries(state.panes).forEach(([exploreId, item]) => {
callback(item!, exploreId as ExploreId);
});
};
export const addHistoryItem = (
@ -130,7 +131,7 @@ export const deleteRichHistory = (): ThunkResult<void> => {
export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
return async (dispatch, getState) => {
const filters = getState().explore![exploreId]?.richHistorySearchFilters;
const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
if (filters) {
const richHistoryResults = await getRichHistory(filters);
dispatch(richHistoryUpdatedAction({ richHistoryResults, exploreId }));
@ -140,8 +141,8 @@ export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
return async (dispatch, getState) => {
const currentFilters = getState().explore![exploreId]?.richHistorySearchFilters;
const currentRichHistory = getState().explore![exploreId]?.richHistory;
const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
const currentRichHistory = getState().explore.panes[exploreId]?.richHistory;
if (currentFilters && currentRichHistory) {
const nextFilters = { ...currentFilters, page: (currentFilters?.page || 1) + 1 };
const moreRichHistory = await getRichHistory(nextFilters);

View File

@ -117,7 +117,7 @@ describe('navigateToExplore', () => {
describe('Explore reducer', () => {
describe('split view', () => {
describe('split close', () => {
it('should keep right pane as left when left is closed', () => {
it('should move right pane to left when left is closed', () => {
const leftItemMock = {
containerWidth: 100,
} as unknown as ExploreItemState;
@ -127,8 +127,10 @@ describe('Explore reducer', () => {
} as unknown as ExploreItemState;
const initialState = {
left: leftItemMock,
right: rightItemMock,
panes: {
left: leftItemMock,
right: rightItemMock,
},
} as unknown as ExploreState;
// closing left item
@ -138,9 +140,10 @@ describe('Explore reducer', () => {
.thenStateShouldEqual({
evenSplitPanes: true,
largerExploreId: undefined,
left: rightItemMock,
panes: {
left: rightItemMock,
},
maxedExploreId: undefined,
right: undefined,
syncedTimes: false,
} as unknown as ExploreState);
});
@ -154,8 +157,10 @@ describe('Explore reducer', () => {
} as unknown as ExploreItemState;
const initialState = {
left: leftItemMock,
right: rightItemMock,
panes: {
left: leftItemMock,
right: rightItemMock,
},
} as unknown as ExploreState;
// closing left item
@ -165,9 +170,10 @@ describe('Explore reducer', () => {
.thenStateShouldEqual({
evenSplitPanes: true,
largerExploreId: undefined,
left: leftItemMock,
panes: {
left: leftItemMock,
},
maxedExploreId: undefined,
right: undefined,
syncedTimes: false,
} as unknown as ExploreState);
});
@ -178,8 +184,10 @@ describe('Explore reducer', () => {
} as unknown as ExploreItemState;
const initialState = {
left: itemMock,
right: itemMock,
panes: {
right: itemMock,
left: itemMock,
},
syncedTimes: true,
} as unknown as ExploreState;
@ -188,8 +196,9 @@ describe('Explore reducer', () => {
.whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.right }))
.thenStateShouldEqual({
evenSplitPanes: true,
left: itemMock,
right: undefined,
panes: {
left: itemMock,
},
syncedTimes: false,
} as unknown as ExploreState);
});

View File

@ -3,18 +3,17 @@ import { AnyAction } from 'redux';
import { ExploreUrlState, serializeStateToUrlParam, SplitOpenOptions, UrlQueryMap } from '@grafana/data';
import { DataSourceSrv, locationService } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
import { PanelModel } from 'app/features/dashboard/state';
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
import { ThunkResult } from '../../../types';
import { createAsyncThunk, ThunkResult } from '../../../types';
import { CorrelationData } from '../../correlations/useCorrelations';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { paneReducer } from './explorePane';
import { initializeExplore, paneReducer } from './explorePane';
import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
//
@ -72,12 +71,12 @@ export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/s
* Not all of the redux state is reflected in URL though.
*/
export const stateSave = (options?: { replace?: boolean }): ThunkResult<void> => {
return (dispatch, getState) => {
const { left, right } = getState().explore;
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));
urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left!));
if (right) {
urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right));
@ -100,9 +99,10 @@ export const lastSavedUrl: UrlQueryMap = {};
* or uses values from options arg. This does only navigation each pane is then responsible for initialization from
* the URL.
*/
export const splitOpen = <T extends DataQuery = DataQuery>(options?: SplitOpenOptions<T>): ThunkResult<void> => {
return async (dispatch, getState) => {
const leftState: ExploreItemState = getState().explore[ExploreId.left];
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;
@ -119,8 +119,8 @@ export const splitOpen = <T extends DataQuery = DataQuery>(options?: SplitOpenOp
const urlState = serializeStateToUrlParam(rightUrlState);
locationService.partial({ right: urlState }, true);
};
};
}
);
/**
* Close the split view and save URL state. We need to update the state here because when closing we cannot just
@ -169,8 +169,9 @@ export const navigateToExplore = (
const initialExploreItemState = makeExplorePaneState();
export const initialExploreState: ExploreState = {
syncedTimes: false,
left: initialExploreItemState,
right: undefined,
panes: {
[ExploreId.left]: initialExploreItemState,
},
correlations: undefined,
richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false,
@ -186,13 +187,12 @@ export const initialExploreState: ExploreState = {
export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
if (splitCloseAction.match(action)) {
const { itemId } = action.payload;
const targetSplit = {
left: itemId === ExploreId.left ? state.right! : state.left,
right: undefined,
const panes = {
left: itemId === ExploreId.left ? state.panes.right : state.panes.left,
};
return {
...state,
...targetSplit,
panes,
largerExploreId: undefined,
maxedExploreId: undefined,
evenSplitPanes: true,
@ -255,18 +255,18 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
}
if (resetExploreAction.match(action)) {
const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right];
stopQueryState(leftState.querySubscription);
if (rightState) {
stopQueryState(rightState.querySubscription);
// 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,
left: {
...initialExploreItemState,
queries: state.left.queries,
panes: {
left: {
...initialExploreItemState,
queries: state.panes.left!.queries,
},
},
};
}
@ -279,12 +279,40 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (splitOpen.pending.match(action)) {
return {
...state,
panes: {
...state.panes,
right: initialExploreItemState,
},
};
}
if (initializeExplore.pending.match(action)) {
return {
...state,
panes: {
...state.panes,
[action.meta.arg.exploreId]: initialExploreItemState,
},
};
}
if (action.payload) {
const { exploreId } = action.payload;
if (exploreId !== undefined) {
// @ts-ignore
const explorePaneState = state[exploreId];
return { ...state, [exploreId]: paneReducer(explorePaneState, action) };
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;
}, {}),
};
}
}

View File

@ -103,7 +103,7 @@ jest.mock('@grafana/runtime', () => ({
}));
function setupQueryResponse(state: StoreState) {
const leftDatasourceInstance = assertIsDefined(state.explore[ExploreId.left].datasourceInstance);
const leftDatasourceInstance = assertIsDefined(state.explore.panes.left!.datasourceInstance);
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(
of({
@ -126,10 +126,12 @@ async function setupStore(queries: DataQuery[], datasourceInstance: Partial<Data
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[exploreId]: {
...defaultInitialState.explore[exploreId],
queries: queries,
datasourceInstance: datasourceInstance,
panes: {
[exploreId]: {
...defaultInitialState.explore.panes[exploreId],
queries: queries,
datasourceInstance: datasourceInstance,
},
},
},
} as unknown as Partial<StoreState>);
@ -157,8 +159,8 @@ describe('runQueries', () => {
setupQueryResponse(getState());
await dispatch(saveCorrelationsAction([]));
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].showMetrics).toBeTruthy();
expect(getState().explore[ExploreId.left].graphResult).toBeDefined();
expect(getState().explore.panes.left!.showMetrics).toBeTruthy();
expect(getState().explore.panes.left!.graphResult).toBeDefined();
});
it('should modify the request-id for all supplementary queries', () => {
@ -167,7 +169,7 @@ describe('runQueries', () => {
dispatch(saveCorrelationsAction([]));
dispatch(runQueries(ExploreId.left));
const state = getState().explore[ExploreId.left];
const state = getState().explore.panes.left!;
expect(state.queryResponse.request?.requestId).toBe('explore_left');
const datasource = state.datasourceInstance as unknown as DataSourceWithSupplementaryQueriesSupport<DataQuery>;
for (const type of supplementaryQueryTypes) {
@ -182,21 +184,21 @@ describe('runQueries', () => {
it('should set state to done if query completes without emitting', async () => {
const { dispatch, getState } = setupTests();
const leftDatasourceInstance = assertIsDefined(getState().explore[ExploreId.left].datasourceInstance);
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
await dispatch(saveCorrelationsAction([]));
await dispatch(runQueries(ExploreId.left));
await new Promise((resolve) => setTimeout(() => resolve(''), 500));
expect(getState().explore[ExploreId.left].queryResponse.state).toBe(LoadingState.Done);
expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done);
});
it('shows results only after correlations are loaded', async () => {
const { dispatch, getState } = setupTests();
setupQueryResponse(getState());
await dispatch(runQueries(ExploreId.left));
expect(getState().explore[ExploreId.left].graphResult).not.toBeDefined();
expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
await dispatch(saveCorrelationsAction([]));
expect(getState().explore[ExploreId.left].graphResult).toBeDefined();
expect(getState().explore.panes.left!.graphResult).toBeDefined();
});
});
@ -207,16 +209,18 @@ describe('running queries', () => {
const exploreId = ExploreId.left;
const initialState = {
explore: {
[exploreId]: {
datasourceInstance: { name: 'testDs' },
initialized: true,
loading: true,
querySubscription: unsubscribable,
queries: ['A'],
range: testRange,
supplementaryQueries: {
[SupplementaryQueryType.LogsVolume]: { enabled: true },
[SupplementaryQueryType.LogsSample]: { enabled: true },
panes: {
[exploreId]: {
datasourceInstance: { name: 'testDs' },
initialized: true,
loading: true,
querySubscription: unsubscribable,
queries: ['A'],
range: testRange,
supplementaryQueries: {
[SupplementaryQueryType.LogsVolume]: { enabled: true },
[SupplementaryQueryType.LogsSample]: { enabled: true },
},
},
},
},
@ -257,10 +261,12 @@ describe('changeQueries', () => {
const { dispatch } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: originalQueries,
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
},
} as unknown as Partial<StoreState>);
@ -289,10 +295,12 @@ describe('changeQueries', () => {
const { dispatch } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
},
},
},
} as unknown as Partial<StoreState>);
@ -316,10 +324,12 @@ describe('changeQueries', () => {
const { dispatch, getState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: originalQueries,
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: originalQueries,
},
},
},
} as unknown as Partial<StoreState>);
@ -331,18 +341,20 @@ describe('changeQueries', () => {
})
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource', datasources[1].getRef());
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('datasource', datasources[1].getRef());
});
it('should not import queries when datasource is not changed', async () => {
const { dispatch, getState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
queries: [{ refId: 'A', datasource: datasources[0].getRef() }],
},
},
},
} as unknown as Partial<StoreState>);
@ -354,9 +366,9 @@ describe('changeQueries', () => {
})
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource', datasources[0].getRef());
expect(getState().explore[ExploreId.left].queries[0]).toEqual({
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'A');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('datasource', datasources[0].getRef());
expect(getState().explore.panes.left!.queries[0]).toEqual({
refId: 'A',
datasource: datasources[0].getRef(),
queryType: 'someValue',
@ -371,9 +383,11 @@ describe('importing queries', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: datasources[0],
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: datasources[0],
},
},
},
} as unknown as Partial<StoreState>);
@ -390,10 +404,10 @@ describe('importing queries', () => {
)
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'refId_A');
expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('refId', 'refId_B');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource.uid', 'ds2');
expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('datasource.uid', 'ds2');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('refId', 'refId_A');
expect(getState().explore.panes.left!.queries[1]).toHaveProperty('refId', 'refId_B');
expect(getState().explore.panes.left!.queries[0]).toHaveProperty('datasource.uid', 'ds2');
expect(getState().explore.panes.left!.queries[1]).toHaveProperty('datasource.uid', 'ds2');
});
});
});
@ -415,10 +429,10 @@ describe('adding new query rows', () => {
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore[exploreId].queries).toHaveLength(1);
expect(getState().explore[exploreId].queryKeys).toEqual(['uid-loki-0']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['uid-loki-0']);
});
it('should add query row when there is not yet a row and meta.mixed === false', async () => {
const queries: DataQuery[] = [];
@ -435,10 +449,10 @@ describe('adding new query rows', () => {
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore[exploreId].queries).toHaveLength(1);
expect(getState().explore[exploreId].queryKeys).toEqual(['uid-loki-0']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['uid-loki-0']);
});
it('should add another query row if there are two rows already', async () => {
@ -462,10 +476,10 @@ describe('adding new query rows', () => {
};
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore[exploreId].queries).toHaveLength(3);
expect(getState().explore[exploreId].queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(3);
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
});
});
describe('with mixed datasources enabled', () => {
@ -490,11 +504,11 @@ describe('adding new query rows', () => {
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore[exploreId].queries).toHaveLength(1);
expect(getState().explore[exploreId].queries[0]?.datasource?.type).toBe('postgres');
expect(getState().explore[exploreId].queryKeys).toEqual(['ds1-0']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('mixed');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(true);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queries[0]?.datasource?.type).toBe('postgres');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds1-0']);
});
it('should add query row whith root ds (with overriding the default ds) when there is not yet a row', async () => {
@ -512,11 +526,11 @@ describe('adding new query rows', () => {
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore[exploreId].queries).toHaveLength(1);
expect(getState().explore[exploreId].queries[0]?.datasource?.type).toBe('loki');
expect(getState().explore[exploreId].queryKeys).toEqual(['uid-loki-0']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('loki');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(1);
expect(getState().explore.panes[exploreId]!.queries[0]?.datasource?.type).toBe('loki');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['uid-loki-0']);
});
it('should add another query row if there are two rows already (impossible in UI)', async () => {
@ -542,11 +556,11 @@ describe('adding new query rows', () => {
const getState = await setupStore(queries, datasourceInstance);
expect(getState().explore[exploreId].datasourceInstance?.meta?.id).toBe('postgres');
expect(getState().explore[exploreId].datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore[exploreId].queries).toHaveLength(3);
expect(getState().explore[exploreId].queries[2]?.datasource?.type).toBe('loki');
expect(getState().explore[exploreId].queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.id).toBe('postgres');
expect(getState().explore.panes[exploreId]!.datasourceInstance?.meta?.mixed).toBe(false);
expect(getState().explore.panes[exploreId]!.queries).toHaveLength(3);
expect(getState().explore.panes[exploreId]!.queries[2]?.datasource?.type).toBe('loki');
expect(getState().explore.panes[exploreId]!.queryKeys).toEqual(['ds3-0', 'ds4-1', 'ds4-2']);
});
});
});
@ -630,20 +644,22 @@ describe('reducer', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
},
absoluteRange: { from: 1621348027000, to: 1621348050000 },
},
absoluteRange: { from: 1621348027000, to: 1621348050000 },
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache(ExploreId.left));
expect(getState().explore[ExploreId.left].cache).toEqual([
expect(getState().explore.panes.left!.cache).toEqual([
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
]);
});
@ -652,44 +668,48 @@ describe('reducer', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
queryResponse: { series: [{ name: 'test name' }], state: LoadingState.Loading },
absoluteRange: { from: 1621348027000, to: 1621348050000 },
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: { series: [{ name: 'test name' }], state: LoadingState.Loading },
absoluteRange: { from: 1621348027000, to: 1621348050000 },
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache(ExploreId.left));
expect(getState().explore[ExploreId.left].cache).toEqual([]);
expect(getState().explore.panes.left!.cache).toEqual([]);
});
it('should not add duplicate response to cache', async () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
},
absoluteRange: { from: 1621348027000, to: 1621348050000 },
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
panes: {
left: {
...defaultInitialState.explore.panes.left,
queryResponse: {
series: [{ name: 'test name' }],
state: LoadingState.Done,
},
],
absoluteRange: { from: 1621348027000, to: 1621348050000 },
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: LoadingState.Done },
},
],
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(addResultsToCache(ExploreId.left));
expect(getState().explore[ExploreId.left].cache).toHaveLength(1);
expect(getState().explore[ExploreId.left].cache).toEqual([
expect(getState().explore.panes.left!.cache).toHaveLength(1);
expect(getState().explore.panes.left!.cache).toEqual([
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'old test name' }], state: 'Done' } },
]);
});
@ -698,21 +718,23 @@ describe('reducer', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: 'Done' },
},
],
panes: {
left: {
...defaultInitialState.explore.panes.left,
cache: [
{
key: 'from=1621348027000&to=1621348050000',
value: { series: [{ name: 'old test name' }], state: 'Done' },
},
],
},
},
},
} as unknown as Partial<StoreState>);
await dispatch(clearCache(ExploreId.left));
expect(getState().explore[ExploreId.left].cache).toEqual([]);
expect(getState().explore.panes.left!.cache).toEqual([]);
});
});
@ -739,22 +761,24 @@ describe('reducer', () => {
const store: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'something',
panes: {
left: {
...defaultInitialState.explore.panes.left,
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
meta: {
id: 'something',
},
getDataProvider: () => {
return mockDataProvider();
},
getSupportedSupplementaryQueryTypes: () => [
SupplementaryQueryType.LogsVolume,
SupplementaryQueryType.LogsSample,
],
getSupplementaryQuery: jest.fn(),
},
getDataProvider: () => {
return mockDataProvider();
},
getSupportedSupplementaryQueryTypes: () => [
SupplementaryQueryType.LogsVolume,
SupplementaryQueryType.LogsSample,
],
getSupplementaryQuery: jest.fn(),
},
},
},
@ -797,8 +821,8 @@ describe('reducer', () => {
expect(unsubscribes[1]).toBeCalled();
for (const type of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[type].dataProvider).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeUndefined();
}
});
@ -814,20 +838,20 @@ describe('reducer', () => {
dispatch(runQueries(ExploreId.left));
for (const type of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data).toBeDefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore[ExploreId.left].supplementaryQueries[type].dataProvider).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
}
for (const type of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data).toBeDefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore[ExploreId.left].supplementaryQueries[type].dataProvider).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data!.state).toBe(LoadingState.Loading);
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
}
dispatch(cancelQueries(ExploreId.left));
for (const type of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
}
});
@ -841,17 +865,17 @@ describe('reducer', () => {
dispatch(runQueries(ExploreId.left));
for (const types of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[types].data).toBeDefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore[ExploreId.left].supplementaryQueries[types].dataProvider).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeDefined();
}
dispatch(cancelQueries(ExploreId.left));
for (const types of supplementaryQueryTypes) {
expect(getState().explore[ExploreId.left].supplementaryQueries[types].data).toBeDefined();
expect(getState().explore[ExploreId.left].supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore[ExploreId.left].supplementaryQueries[types].dataProvider).toBeUndefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[types].data!.state).toBe(LoadingState.Done);
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeUndefined();
}
});
@ -861,34 +885,30 @@ describe('reducer', () => {
};
// turn logs volume off (but keep logs sample on)
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsVolume));
expect(getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
false
);
expect(getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(
true
);
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(true);
// verify that if we run a query, it will: 1) not do logs volume, 2) do logs sample 3) provider will still be set for both
dispatch(runQueries(ExploreId.left));
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].data
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].data
).toBeUndefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
).toBeUndefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
).toBeDefined();
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].data).toBeDefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].data
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
).toBeDefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
).toBeDefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
).toBeDefined();
});
@ -900,30 +920,28 @@ describe('reducer', () => {
// runQueries sets up providers, but does not run queries
dispatch(runQueries(ExploreId.left));
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
).toBeDefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataProvider
).toBeDefined();
// we turn 1 supplementary query (logs volume) on
dispatch(setSupplementaryQueryEnabled(ExploreId.left, true, SupplementaryQueryType.LogsVolume));
// verify it was turned on
expect(getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
true
);
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(true);
// verify that other stay off
expect(getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(
false
);
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataSubscription
).toBeDefined();
expect(
getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].dataSubscription
).toBeUndefined();
});
});
@ -934,24 +952,26 @@ describe('reducer', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: logRows,
panes: {
[ExploreId.left]: {
...defaultInitialState.explore.panes[ExploreId.left],
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: logRows,
},
},
},
},
} as unknown as Partial<StoreState>);
expect(getState().explore[ExploreId.left].logsResult?.rows.length).toBe(logRows.length);
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(logRows.length);
await dispatch(clearLogs({ exploreId: ExploreId.left }));
expect(getState().explore[ExploreId.left].logsResult?.rows.length).toBe(0);
expect(getState().explore[ExploreId.left].clearedAtIndex).toBe(logRows.length - 1);
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(0);
expect(getState().explore.panes[ExploreId.left]?.clearedAtIndex).toBe(logRows.length - 1);
});
it('should filter new log rows', async () => {
@ -962,21 +982,23 @@ describe('reducer', () => {
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
isLive: true,
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: oldLogRows,
panes: {
[ExploreId.left]: {
...defaultInitialState.explore.panes[ExploreId.left],
isLive: true,
queryResponse: {
state: LoadingState.Streaming,
},
logsResult: {
hasUniqueLabels: false,
rows: oldLogRows,
},
},
},
},
} as unknown as Partial<StoreState>);
expect(getState().explore[ExploreId.left].logsResult?.rows.length).toBe(oldLogRows.length);
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(oldLogRows.length);
await dispatch(clearLogs({ exploreId: ExploreId.left }));
await dispatch(
@ -996,8 +1018,8 @@ describe('reducer', () => {
} as unknown as QueryEndedPayload)
);
expect(getState().explore[ExploreId.left].logsResult?.rows.length).toBe(newLogRows.length);
expect(getState().explore[ExploreId.left].clearedAtIndex).toBe(oldLogRows.length - 1);
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(newLogRows.length);
expect(getState().explore.panes[ExploreId.left]?.clearedAtIndex).toBe(oldLogRows.length - 1);
});
});
});

View File

@ -241,13 +241,13 @@ export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCa
*/
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return async (dispatch, getState) => {
const queries = getState().explore[exploreId]!.queries;
const queries = getState().explore.panes[exploreId]!.queries;
let datasourceOverride = undefined;
// if this is the first query being added, check for a root datasource
// if it's not mixed, send it as an override. generateEmptyQuery doesn't have access to state
if (queries.length === 0) {
const rootDatasource = getState().explore[exploreId]!.datasourceInstance;
const rootDatasource = getState().explore.panes[exploreId]!.datasourceInstance;
if (!config.featureToggles.exploreMixedDatasource || !rootDatasource?.meta.mixed) {
datasourceOverride = rootDatasource;
}
@ -267,7 +267,7 @@ export function cancelQueries(exploreId: ExploreId): ThunkResult<void> {
dispatch(scanStopAction({ exploreId }));
dispatch(cancelQueriesAction({ exploreId }));
const supplementaryQueries = getState().explore[exploreId]!.supplementaryQueries;
const supplementaryQueries = getState().explore.panes[exploreId]!.supplementaryQueries;
// Cancel all data providers
for (const type of supplementaryQueryTypes) {
dispatch(cleanSupplementaryQueryDataProviderAction({ exploreId, type }));
@ -311,7 +311,7 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
'explore/changeQueries',
async ({ queries, exploreId }, { getState, dispatch }) => {
let queriesImported = false;
const oldQueries = getState().explore[exploreId]!.queries;
const oldQueries = getState().explore.panes[exploreId]!.queries;
for (const newQuery of queries) {
for (const oldQuery of oldQueries) {
@ -423,7 +423,7 @@ export function modifyQueries(
modifier: (query: DataQuery, modification: QueryFixAction) => Promise<DataQuery>
): ThunkResult<void> {
return async (dispatch, getState) => {
const state = getState().explore[exploreId]!;
const state = getState().explore.panes[exploreId]!;
const { queries } = state;
@ -457,8 +457,9 @@ async function handleHistory(
// Because filtering happens in the backend we cannot add a new entry without checking if it matches currently
// used filters. Instead, we refresh the query history list.
// TODO: run only if Query History list is opened (#47252)
await dispatch(loadRichHistory(ExploreId.left));
await dispatch(loadRichHistory(ExploreId.right));
for (const exploreId in state.panes) {
await dispatch(loadRichHistory(exploreId as ExploreId));
}
}
/**
@ -479,7 +480,7 @@ export const runQueries = (
dispatch(clearCache(exploreId));
}
const exploreItemState = getState().explore[exploreId]!;
const exploreItemState = getState().explore.panes[exploreId]!;
const {
datasourceInstance,
containerWidth,
@ -582,9 +583,9 @@ export const runQueries = (
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
// Keep scanning for results if this was the last scanning transaction
if (getState().explore[exploreId]!.scanning) {
if (getState().explore.panes[exploreId]!.scanning) {
if (data.state === LoadingState.Done && data.series.length === 0) {
const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
const range = getShiftedTimeRange(-1, getState().explore.panes[exploreId]!.range);
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId));
} else {
@ -602,7 +603,7 @@ export const runQueries = (
// In case we don't get any response at all but the observable completed, make sure we stop loading state.
// This is for cases when some queries are noop like running first query after load but we don't have any
// actual query input.
if (getState().explore[exploreId]!.queryResponse.state === LoadingState.Loading) {
if (getState().explore.panes[exploreId]!.queryResponse.state === LoadingState.Loading) {
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Done }));
}
},
@ -777,7 +778,7 @@ function canReuseSupplementaryQueryData(
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
return (dispatch, getState) => {
// Inject react keys into query objects
const queries = getState().explore[exploreId]!.queries;
const queries = getState().explore.panes[exploreId]!.queries;
const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index));
dispatch(setQueriesAction({ exploreId, queries: nextQueries }));
dispatch(runQueries(exploreId));
@ -794,7 +795,7 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
// Register the scanner
dispatch(scanStartAction({ exploreId }));
// Scanning must trigger query run, and return the new range
const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
const range = getShiftedTimeRange(-1, getState().explore.panes[exploreId]!.range);
// Set the new range to be displayed
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId));
@ -803,8 +804,8 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
export function addResultsToCache(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const queryResponse = getState().explore[exploreId]!.queryResponse;
const absoluteRange = getState().explore[exploreId]!.absoluteRange;
const queryResponse = getState().explore.panes[exploreId]!.queryResponse;
const absoluteRange = getState().explore.panes[exploreId]!.absoluteRange;
const cacheKey = createCacheKey(absoluteRange);
// Save results to cache only when all results received and loading is done
@ -825,7 +826,7 @@ export function clearCache(exploreId: ExploreId): ThunkResult<void> {
*/
export function loadSupplementaryQueryData(exploreId: ExploreId, type: SupplementaryQueryType): ThunkResult<void> {
return (dispatch, getState) => {
const { supplementaryQueries } = getState().explore[exploreId]!;
const { supplementaryQueries } = getState().explore.panes[exploreId]!;
const dataProvider = supplementaryQueries[type].dataProvider;
if (dataProvider) {

View File

@ -1,5 +1,5 @@
import { ExploreId, StoreState } from 'app/types';
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]);
export const isSplit = (state: StoreState) => Object.keys(state.explore.panes).length > 1;
export const getExploreItemSelector = (exploreId: ExploreId) => (state: StoreState) => state.explore[exploreId];
export const getExploreItemSelector = (exploreId: ExploreId) => (state: StoreState) => state.explore.panes[exploreId];

View File

@ -2,7 +2,7 @@ import { reducerTester } from 'test/core/redux/reducerTester';
import { dateTime, LoadingState } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { ExploreId, ExploreItemState } from 'app/types';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';

View File

@ -43,12 +43,10 @@ export const updateTimeRange = (options: {
return (dispatch, getState) => {
const { syncedTimes } = getState().explore;
if (syncedTimes) {
dispatch(updateTime({ ...options, exploreId: ExploreId.left }));
// When running query by updating time range, we want to preserve cache.
// Cached results are currently used in Logs pagination.
dispatch(runQueries(ExploreId.left, { preserveCache: true }));
dispatch(updateTime({ ...options, exploreId: ExploreId.right }));
dispatch(runQueries(ExploreId.right, { preserveCache: true }));
Object.keys(getState().explore.panes).forEach((exploreId) => {
dispatch(updateTime({ ...options, exploreId: exploreId as ExploreId }));
dispatch(runQueries(exploreId as ExploreId, { preserveCache: true }));
});
} else {
dispatch(updateTime({ ...options }));
dispatch(runQueries(options.exploreId, { preserveCache: true }));
@ -73,7 +71,7 @@ export const updateTime = (config: {
}): ThunkResult<void> => {
return (dispatch, getState) => {
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
const itemState = getState().explore[exploreId]!;
const itemState = getState().explore.panes[exploreId]!;
const timeZone = getTimeZone(getState().user);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
const { range: rangeInState } = itemState;
@ -118,13 +116,14 @@ export const updateTime = (config: {
*/
export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
if (exploreId === ExploreId.left) {
const leftState = getState().explore.left;
dispatch(updateTimeRange({ exploreId: ExploreId.right, rawRange: leftState.range.raw }));
} else {
const rightState = getState().explore.right!;
dispatch(updateTimeRange({ exploreId: ExploreId.left, rawRange: rightState.range.raw }));
}
const range = getState().explore.panes[exploreId]!.range.raw;
Object.keys(getState().explore.panes)
.filter((key) => key !== exploreId)
.forEach((exploreId) => {
dispatch(updateTimeRange({ exploreId: exploreId as ExploreId, rawRange: range }));
});
const isTimeSynced = getState().explore.syncedTimes;
dispatch(syncTimesAction({ syncedTimes: !isTimeSynced }));
dispatch(stateSave());
@ -140,16 +139,13 @@ export function makeAbsoluteTime(): ThunkResult<void> {
return (dispatch, getState) => {
const timeZone = getTimeZone(getState().user);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
const leftState = getState().explore.left;
const leftRange = getTimeRange(timeZone, leftState.range.raw, fiscalYearStartMonth);
const leftAbsoluteRange: AbsoluteTimeRange = { from: leftRange.from.valueOf(), to: leftRange.to.valueOf() };
dispatch(updateTime({ exploreId: ExploreId.left, absoluteRange: leftAbsoluteRange }));
const rightState = getState().explore.right!;
if (rightState) {
const rightRange = getTimeRange(timeZone, rightState.range.raw, fiscalYearStartMonth);
const rightAbsoluteRange: AbsoluteTimeRange = { from: rightRange.from.valueOf(), to: rightRange.to.valueOf() };
dispatch(updateTime({ exploreId: ExploreId.right, absoluteRange: rightAbsoluteRange }));
}
Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => {
const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth);
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
dispatch(updateTime({ exploreId: exploreId as ExploreId, absoluteRange }));
});
dispatch(stateSave());
};
}

View File

@ -38,14 +38,14 @@ export interface ExploreState {
* True if time interval for panels are synced. Only possible with split mode.
*/
syncedTimes: boolean;
/**
* Explore state of the left split (left is default in non-split view).
*/
left: ExploreItemState;
/**
* Explore state of the right area in split view.
*/
right?: ExploreItemState;
// This being optional wouldn't be needed with noUncheckedIndexedAccess set to true, but it cause more than 5k errors currently.
// In order to be safe, we declare each item as pssobly undefined to force existence checks.
// This will have the side effect of also forcing undefined checks when iterating over this object entries, but
// it's better to error on the safer side.
panes: {
[paneId in ExploreId]?: ExploreItemState;
};
correlations?: CorrelationData[];
@ -68,12 +68,12 @@ export interface ExploreState {
/**
* On a split manual resize, we calculate which pane is larger, or if they are roughly the same size. If undefined, it is not split or they are roughly the same size
*/
largerExploreId?: ExploreId;
largerExploreId?: keyof ExploreState['panes'];
/**
* If a maximize pane button is pressed, this indicates which side was maximized. Will be undefined if not split or if it is manually resized
*/
maxedExploreId?: ExploreId;
maxedExploreId?: keyof ExploreState['panes'];
/**
* If a minimize pane button is pressed, it will do an even split of panes. Will be undefined if split or on a manual resize