From f03d0698d3faa756004c8766290fffe4131a293c Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Wed, 3 May 2023 16:45:11 +0100 Subject: [PATCH] Explore: move panes into a keyed object (#66117) --- .betterer.results | 19 +- packages/grafana-data/src/types/explore.ts | 4 +- public/app/core/utils/explore.test.ts | 2 +- .../explore/AddToDashboard/index.test.tsx | 10 +- public/app/features/explore/Explore.test.tsx | 4 +- public/app/features/explore/Explore.tsx | 4 +- public/app/features/explore/ExplorePage.tsx | 4 +- .../features/explore/ExplorePaneContainer.tsx | 16 +- .../explore/ExploreQueryInspector.tsx | 2 +- .../app/features/explore/ExploreToolbar.tsx | 6 +- public/app/features/explore/LogsContainer.tsx | 5 +- .../explore/NodeGraphContainer.test.tsx | 3 +- .../features/explore/NodeGraphContainer.tsx | 2 +- .../app/features/explore/QueryRows.test.tsx | 13 +- .../explore/RawPrometheusContainer.test.tsx | 2 +- .../explore/RawPrometheusContainer.tsx | 2 +- .../explore/ResponseErrorContainer.test.tsx | 4 +- .../explore/ResponseErrorContainer.tsx | 2 +- .../explore/RichHistory/RichHistory.test.tsx | 3 +- .../RichHistory/RichHistoryCard.test.tsx | 2 +- .../explore/RichHistory/RichHistoryCard.tsx | 2 +- .../RichHistory/RichHistoryContainer.test.tsx | 3 +- .../RichHistory/RichHistoryContainer.tsx | 3 +- .../RichHistoryStarredTab.test.tsx | 3 +- .../features/explore/TableContainer.test.tsx | 2 +- .../app/features/explore/TableContainer.tsx | 3 +- .../features/explore/TraceView/TraceView.tsx | 4 +- .../explore/TraceView/TraceViewContainer.tsx | 2 +- .../explore/spec/queryHistory.test.tsx | 2 +- .../app/features/explore/state/datasource.ts | 6 +- .../explore/state/explorePane.test.ts | 20 +- .../app/features/explore/state/explorePane.ts | 62 ++- public/app/features/explore/state/helpers.ts | 62 +-- public/app/features/explore/state/history.ts | 11 +- .../app/features/explore/state/main.test.ts | 35 +- public/app/features/explore/state/main.ts | 84 ++-- .../app/features/explore/state/query.test.ts | 394 +++++++++--------- public/app/features/explore/state/query.ts | 33 +- .../app/features/explore/state/selectors.ts | 4 +- .../app/features/explore/state/time.test.ts | 2 +- public/app/features/explore/state/time.ts | 44 +- public/app/types/explore.ts | 20 +- 42 files changed, 501 insertions(+), 409 deletions(-) diff --git a/.betterer.results b/.betterer.results index a641a30be82..5ae9c8d69b2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index 57568f95f2f..9ca6e18fab7 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -22,7 +22,7 @@ export interface ExploreTracePanelState { spanId?: string; } -export interface SplitOpenOptions { +export interface SplitOpenOptions { datasourceUid: string; /** @deprecated Will be removed in a future version. Use queries instead. */ query?: T; @@ -34,4 +34,4 @@ export interface SplitOpenOptions { /** * SplitOpen type is used in Explore and related components. */ -export type SplitOpen = (options?: SplitOpenOptions | undefined) => void; +export type SplitOpen = (options?: SplitOpenOptions | undefined) => void; diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index d6de87958da..5d11ed7c943 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -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, diff --git a/public/app/features/explore/AddToDashboard/index.test.tsx b/public/app/features/explore/AddToDashboard/index.test.tsx index b1a36dcb248..99b96961d6b 100644 --- a/public/app/features/explore/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/AddToDashboard/index.test.tsx @@ -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({children}); diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index fd726b323f7..79374953c7a 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -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(), diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index c85b9494459..e27a4ba2f99 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -253,7 +253,7 @@ export class Explore extends React.PureComponent { }; onSplitOpen = (panelType: string) => { - return async (options?: SplitOpenOptions) => { + 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 { 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, diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 68458025230..78b885b479d 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -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}`; diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 9744d8a07fd..51c82652def 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -122,16 +122,16 @@ class ExplorePaneContainerUnconnected extends React.PureComponent { 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, diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 6084734bbf8..ea2bb312217 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -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 { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 73abdae0d93..adab7756315 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -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( diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 67d89101449..73f76fd680f 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -208,10 +208,9 @@ class LogsContainer extends PureComponent { } } -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, diff --git a/public/app/features/explore/NodeGraphContainer.test.tsx b/public/app/features/explore/NodeGraphContainer.test.tsx index fc991d5eaa0..d37d63a513c 100644 --- a/public/app/features/explore/NodeGraphContainer.test.tsx +++ b/public/app/features/explore/NodeGraphContainer.test.tsx @@ -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'; diff --git a/public/app/features/explore/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraphContainer.tsx index 2594f757a13..a55d71636d1 100644 --- a/public/app/features/explore/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraphContainer.tsx @@ -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, }; } diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 2450774c14c..c04019c4283 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -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, }; diff --git a/public/app/features/explore/RawPrometheusContainer.test.tsx b/public/app/features/explore/RawPrometheusContainer.test.tsx index 2f750d7f752..d218394c2ca 100644 --- a/public/app/features/explore/RawPrometheusContainer.test.tsx +++ b/public/app/features/explore/RawPrometheusContainer.test.tsx @@ -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'; diff --git a/public/app/features/explore/RawPrometheusContainer.tsx b/public/app/features/explore/RawPrometheusContainer.tsx index c37a9859852..517eb695712 100644 --- a/public/app/features/explore/RawPrometheusContainer.tsx +++ b/public/app/features/explore/RawPrometheusContainer.tsx @@ -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; diff --git a/public/app/features/explore/ResponseErrorContainer.test.tsx b/public/app/features/explore/ResponseErrorContainer.test.tsx index dec33a6bf35..bc59f4fe513 100644 --- a/public/app/features/explore/ResponseErrorContainer.test.tsx +++ b/public/app/features/explore/ResponseErrorContainer.test.tsx @@ -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, diff --git a/public/app/features/explore/ResponseErrorContainer.tsx b/public/app/features/explore/ResponseErrorContainer.tsx index ec6f311892f..6dfd2c227c4 100644 --- a/public/app/features/explore/ResponseErrorContainer.tsx +++ b/public/app/features/explore/ResponseErrorContainer.tsx @@ -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 diff --git a/public/app/features/explore/RichHistory/RichHistory.test.tsx b/public/app/features/explore/RichHistory/RichHistory.test.tsx index ea6b16fba9f..fa8755c7a3f 100644 --- a/public/app/features/explore/RichHistory/RichHistory.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.test.tsx @@ -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'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index 63cccb46522..27a5808d8e7 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -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'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index bd81b1ed3d5..54fc5214f0b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -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, diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx index 84d1aca02a9..813752ade03 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.test.tsx @@ -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'; diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index 72fb7c125f1..f89e30757c0 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -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; diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx index 31f18b06b7c..d978e8e4348 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx @@ -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'; diff --git a/public/app/features/explore/TableContainer.test.tsx b/public/app/features/explore/TableContainer.test.tsx index 7515d93906a..a227a93f1f0 100644 --- a/public/app/features/explore/TableContainer.test.tsx +++ b/public/app/features/explore/TableContainer.test.tsx @@ -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'; diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index 600c4cdd7be..767f801f6e9 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -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 }; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 35d87ca9e14..06c927fa5b1 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -256,7 +256,7 @@ function useFocusSpanLink(options: { refId?: string; datasource?: DataSourceApi; }): [string | undefined, (traceId: string, spanId: string) => LinkModel] { - 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) => { diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index b11e1d4263d..7572570cbf8 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -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'; diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 952b598c9e2..83b15dbb406 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -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, diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index 5aed7e998ed..a78a3d358f7 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -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)); } diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index 02debf075c0..d4bf58ff10b 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -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 }) ) ); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index b3d7878864d..6bf30d81e4c 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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 { 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('explore/initializeExplore'); +const initializeExploreAction = createAction('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 { - 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 { 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; } diff --git a/public/app/features/explore/state/helpers.ts b/public/app/features/explore/state/helpers.ts index 511e24acc81..1437bb0a84c 100644 --- a/public/app/features/explore/state/helpers.ts +++ b/public/app/features/explore/state/helpers.ts @@ -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, + }, }, }, }, diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index 3590dda1df3..8c46f0dce72 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -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 => { export const loadRichHistory = (exploreId: ExploreId): ThunkResult => { 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 => { export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult => { 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); diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index a169d52bd6a..27cf6af3227 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -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); }); diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 18153a8e6de..c51591cc705 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -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('explore/s * Not all of the redux state is reflected in URL though. */ export const stateSave = (options?: { replace?: boolean }): ThunkResult => { - 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 = (options?: SplitOpenOptions): ThunkResult => { - 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 = (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((acc, [id, pane]) => { + if (id === exploreId) { + acc[id as ExploreId] = paneReducer(pane, action); + } else { + acc[id as ExploreId] = pane; + } + return acc; + }, {}), + }; } } diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index c764e0dbc29..81bf80177cf 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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 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); @@ -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; 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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); 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); 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); 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); 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); - 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); - 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); }); }); }); diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 1b9ecacc758..a1cb06bac8b 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -241,13 +241,13 @@ export const clearCacheAction = createAction('explore/clearCa */ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult { 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 { 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( '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 ): ThunkResult { 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 { 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 { // 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 { export function addResultsToCache(exploreId: ExploreId): ThunkResult { 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 { */ export function loadSupplementaryQueryData(exploreId: ExploreId, type: SupplementaryQueryType): ThunkResult { return (dispatch, getState) => { - const { supplementaryQueries } = getState().explore[exploreId]!; + const { supplementaryQueries } = getState().explore.panes[exploreId]!; const dataProvider = supplementaryQueries[type].dataProvider; if (dataProvider) { diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index 0fee6ab2be6..18d2767cab9 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -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]; diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index b32c38c6f8a..bc561d0ff69 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -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'; diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index ec96b5b5600..d298e68310b 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -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 => { 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 { 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 { 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()); }; } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 6c523946588..351c88ddbb5 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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