mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Discover] Handle ES|QL mode when switching tabs (#217600)
## Summary This PR updates the handling of ES|QL mode fetches so that they continue to run in the background when switching tabs, and the tab state is properly updated when a fetch completes (even if not in the selected tab). The main changes include the following: - Removes the `useEsqlMode` hook entirely and migrates the logic to a `buildEsqlFetchSubscribe` function that's subscribed to directly in `DiscoverDataStateContainer`, and tied to the lifetime of the state container. - Modifies the `updateTabs` logic to remove the dependency on raw URL state for persisting/restoring tab state, and instead rely directly on `DiscoverAppState` and a new internal state `lastPersistedGlobalState` property. This was done because tab state updates can now happen after changing tabs, and the raw URL state properties can become out of sync in that case unless we manually sync them, which would be brittle. It also removes duplicate state which we no longer need since we can just update the URL directly from the state when switching to a tab. - Updates `replaceUrlState` in `DiscoverAppStateContainer` to _not_ update the URL if its associated tab is not selected, and instead just update the app state directly when unselected, to prevent leaking URL state updates between tabs. - Moves `TABS_ENABLED` to the main Discover `constants` file as suggested in a previous PR review to avoid circular dependencies. - A couple of small cleanups related to `unsafeCurrentId`. Part of #216475. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
f6ad013220
commit
bcba741abc
17 changed files with 327 additions and 317 deletions
|
@ -58,7 +58,7 @@ import { PanelsToggle } from '../../../../components/panels_toggle';
|
|||
import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux';
|
||||
import { TABS_ENABLED } from '../../discover_main_route';
|
||||
import { TABS_ENABLED } from '../../../../constants';
|
||||
|
||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
|
|
|
@ -17,7 +17,6 @@ import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
|||
import { useSavedSearchAliasMatchRedirect } from '../../../../hooks/saved_search_alias_match_redirect';
|
||||
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
|
||||
import { useAdHocDataViews } from '../../hooks/use_adhoc_data_views';
|
||||
import { useEsqlMode } from '../../hooks/use_esql_mode';
|
||||
|
||||
const DiscoverLayoutMemoized = React.memo(DiscoverLayout);
|
||||
|
||||
|
@ -38,14 +37,6 @@ export function DiscoverMainApp({ stateContainer }: DiscoverMainProps) {
|
|||
*/
|
||||
useAdHocDataViews();
|
||||
|
||||
/**
|
||||
* State changes (data view, columns), when a text base query result is returned
|
||||
*/
|
||||
useEsqlMode({
|
||||
dataViews: services.dataViews,
|
||||
stateContainer,
|
||||
});
|
||||
|
||||
/**
|
||||
* SavedSearch dependent initializing
|
||||
*/
|
||||
|
|
|
@ -34,9 +34,7 @@ import {
|
|||
} from './components/session_view';
|
||||
import { useAsyncFunction } from './hooks/use_async_function';
|
||||
import { TabsView } from './components/tabs_view';
|
||||
|
||||
// TEMPORARY: This is a temporary flag to enable/disable tabs in Discover until the feature is fully implemented.
|
||||
export const TABS_ENABLED = false;
|
||||
import { TABS_ENABLED } from '../../constants';
|
||||
|
||||
export interface MainRouteProps {
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { hasTransformationalCommand, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { useSavedSearchInitial } from '../state_management/discover_state_provider';
|
||||
import type { DiscoverStateContainer } from '../state_management/discover_state';
|
||||
import { getValidViewMode } from '../utils/get_valid_view_mode';
|
||||
import { FetchStatus } from '../../types';
|
||||
import {
|
||||
internalStateActions,
|
||||
useCurrentTabAction,
|
||||
useInternalStateDispatch,
|
||||
} from '../state_management/redux';
|
||||
|
||||
const MAX_NUM_OF_COLUMNS = 50;
|
||||
|
||||
/**
|
||||
* Hook to take care of ES|QL state transformations when a new result is returned
|
||||
* If necessary this is setting displayed columns and selected data view
|
||||
*/
|
||||
export function useEsqlMode({
|
||||
dataViews,
|
||||
stateContainer,
|
||||
}: {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
dataViews: DataViewsContract;
|
||||
}) {
|
||||
const setResetDefaultProfileState = useCurrentTabAction(
|
||||
internalStateActions.setResetDefaultProfileState
|
||||
);
|
||||
const dispatch = useInternalStateDispatch();
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const prev = useRef<{
|
||||
initialFetch: boolean;
|
||||
query: string;
|
||||
allColumns: string[];
|
||||
defaultColumns: string[];
|
||||
}>({
|
||||
initialFetch: true,
|
||||
query: '',
|
||||
allColumns: [],
|
||||
defaultColumns: [],
|
||||
});
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (!prev.current.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cleanup when it's not an ES|QL query
|
||||
prev.current = {
|
||||
initialFetch: true,
|
||||
query: '',
|
||||
allColumns: [],
|
||||
defaultColumns: [],
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = stateContainer.dataState.data$.documents$
|
||||
.pipe(
|
||||
switchMap(async (next) => {
|
||||
const { query: nextQuery } = next;
|
||||
|
||||
if (!nextQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOfAggregateQueryType(nextQuery)) {
|
||||
// cleanup for a "regular" query
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to reset the default profile state on index pattern changes
|
||||
// when loading starts to ensure the correct pre fetch state is available
|
||||
// before data fetching is triggered
|
||||
if (next.fetchStatus === FetchStatus.LOADING) {
|
||||
// We have to grab the current query from appState
|
||||
// here since nextQuery has not been updated yet
|
||||
const appStateQuery = stateContainer.appState.getState().query;
|
||||
|
||||
if (isOfAggregateQueryType(appStateQuery)) {
|
||||
if (prev.current.initialFetch) {
|
||||
prev.current.query = appStateQuery.esql;
|
||||
}
|
||||
|
||||
const indexPatternChanged =
|
||||
getIndexPatternFromESQLQuery(appStateQuery.esql) !==
|
||||
getIndexPatternFromESQLQuery(prev.current.query);
|
||||
|
||||
// Reset all default profile state when index pattern changes
|
||||
if (indexPatternChanged) {
|
||||
dispatch(
|
||||
setResetDefaultProfileState({
|
||||
resetDefaultProfileState: {
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
breakdownField: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (next.fetchStatus === FetchStatus.ERROR) {
|
||||
// An error occurred, but it's still considered an initial fetch
|
||||
prev.current.initialFetch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (next.fetchStatus !== FetchStatus.PARTIAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextAllColumns = prev.current.allColumns;
|
||||
let nextDefaultColumns = prev.current.defaultColumns;
|
||||
|
||||
if (next.result?.length) {
|
||||
nextAllColumns = Object.keys(next.result[0].raw);
|
||||
|
||||
if (hasTransformationalCommand(nextQuery.esql)) {
|
||||
nextDefaultColumns = nextAllColumns.slice(0, MAX_NUM_OF_COLUMNS);
|
||||
} else {
|
||||
nextDefaultColumns = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (prev.current.initialFetch) {
|
||||
prev.current.initialFetch = false;
|
||||
prev.current.query = nextQuery.esql;
|
||||
prev.current.allColumns = nextAllColumns;
|
||||
prev.current.defaultColumns = nextDefaultColumns;
|
||||
}
|
||||
|
||||
const indexPatternChanged =
|
||||
getIndexPatternFromESQLQuery(nextQuery.esql) !==
|
||||
getIndexPatternFromESQLQuery(prev.current.query);
|
||||
|
||||
const allColumnsChanged = !isEqual(nextAllColumns, prev.current.allColumns);
|
||||
|
||||
const changeDefaultColumns =
|
||||
indexPatternChanged || !isEqual(nextDefaultColumns, prev.current.defaultColumns);
|
||||
|
||||
const { viewMode } = stateContainer.appState.getState();
|
||||
const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true });
|
||||
|
||||
// If the index pattern hasn't changed, but the available columns have changed
|
||||
// due to transformational commands, reset the associated default profile state
|
||||
if (!indexPatternChanged && allColumnsChanged) {
|
||||
dispatch(
|
||||
setResetDefaultProfileState({
|
||||
resetDefaultProfileState: {
|
||||
columns: true,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prev.current.allColumns = nextAllColumns;
|
||||
|
||||
if (indexPatternChanged || changeDefaultColumns || changeViewMode) {
|
||||
prev.current.query = nextQuery.esql;
|
||||
prev.current.defaultColumns = nextDefaultColumns;
|
||||
|
||||
// just change URL state if necessary
|
||||
if (changeDefaultColumns || changeViewMode) {
|
||||
const nextState = {
|
||||
...(changeDefaultColumns && { columns: nextDefaultColumns }),
|
||||
...(changeViewMode && { viewMode: undefined }),
|
||||
};
|
||||
|
||||
await stateContainer.appState.replaceUrlState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
stateContainer.dataState.data$.documents$.next({
|
||||
...next,
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
// cleanup for e.g. when savedSearch is switched
|
||||
cleanup();
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [dataViews, stateContainer, savedSearch, cleanup, dispatch, setResetDefaultProfileState]);
|
||||
}
|
|
@ -58,11 +58,12 @@ describe('Test discover app state container', () => {
|
|||
internalState,
|
||||
});
|
||||
getCurrentTab = () =>
|
||||
selectTab(internalState.getState(), internalState.getState().tabs.allIds[0]);
|
||||
selectTab(internalState.getState(), internalState.getState().tabs.unsafeCurrentId);
|
||||
});
|
||||
|
||||
const getStateContainer = () =>
|
||||
getDiscoverAppStateContainer({
|
||||
tabId: getCurrentTab().id,
|
||||
stateStorage,
|
||||
internalState,
|
||||
savedSearchContainer: savedSearchState,
|
||||
|
|
|
@ -181,12 +181,14 @@ export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelec
|
|||
* @param services
|
||||
*/
|
||||
export const getDiscoverAppStateContainer = ({
|
||||
tabId,
|
||||
stateStorage,
|
||||
internalState,
|
||||
savedSearchContainer,
|
||||
services,
|
||||
injectCurrentTab,
|
||||
}: {
|
||||
tabId: string;
|
||||
stateStorage: IKbnUrlStateStorage;
|
||||
internalState: InternalStateStore;
|
||||
savedSearchContainer: DiscoverSavedSearchContainer;
|
||||
|
@ -245,7 +247,11 @@ export const getDiscoverAppStateContainer = ({
|
|||
const replaceUrlState = async (newPartial: DiscoverAppState = {}, merge = true) => {
|
||||
addLog('[appState] replaceUrlState', { newPartial, merge });
|
||||
const state = merge ? { ...enhancedAppContainer.getState(), ...newPartial } : newPartial;
|
||||
await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
|
||||
if (internalState.getState().tabs.unsafeCurrentId === tabId) {
|
||||
await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
|
||||
} else {
|
||||
enhancedAppContainer.set(state);
|
||||
}
|
||||
};
|
||||
|
||||
const startAppStateUrlSync = () => {
|
||||
|
|
|
@ -8,11 +8,20 @@
|
|||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, filter, map, mergeMap, ReplaySubject, share, Subject, tap } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
ReplaySubject,
|
||||
share,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public';
|
||||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
@ -32,6 +41,8 @@ import { getFetch$ } from '../data_fetching/get_fetch_observable';
|
|||
import { getDefaultProfileState } from './utils/get_default_profile_state';
|
||||
import type { InternalStateStore, RuntimeStateManager, TabActionInjector, TabState } from './redux';
|
||||
import { internalStateActions, selectTabRuntimeState } from './redux';
|
||||
import { buildEsqlFetchSubscribe } from './utils/build_esql_fetch_subscribe';
|
||||
import type { DiscoverSavedSearchContainer } from './discover_saved_search_container';
|
||||
|
||||
export interface SavedSearchData {
|
||||
main$: DataMain$;
|
||||
|
@ -123,6 +134,7 @@ export interface DiscoverDataStateContainer {
|
|||
*/
|
||||
getInitialFetchStatus: () => FetchStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container responsible for fetching of data in Discover Main
|
||||
* Either by triggering requests to Elasticsearch directly, or by
|
||||
|
@ -134,7 +146,7 @@ export function getDataStateContainer({
|
|||
appStateContainer,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
getSavedSearch,
|
||||
savedSearchContainer,
|
||||
setDataView,
|
||||
injectCurrentTab,
|
||||
getCurrentTab,
|
||||
|
@ -144,7 +156,7 @@ export function getDataStateContainer({
|
|||
appStateContainer: DiscoverAppStateContainer;
|
||||
internalState: InternalStateStore;
|
||||
runtimeStateManager: RuntimeStateManager;
|
||||
getSavedSearch: () => SavedSearch;
|
||||
savedSearchContainer: DiscoverSavedSearchContainer;
|
||||
setDataView: (dataView: DataView) => void;
|
||||
injectCurrentTab: TabActionInjector;
|
||||
getCurrentTab: () => TabState;
|
||||
|
@ -164,7 +176,7 @@ export function getDataStateContainer({
|
|||
const getInitialFetchStatus = () => {
|
||||
const shouldSearchOnPageLoad =
|
||||
uiSettings.get<boolean>(SEARCH_ON_PAGE_LOAD_SETTING) ||
|
||||
getSavedSearch().id !== undefined ||
|
||||
savedSearchContainer.getState().id !== undefined ||
|
||||
!timefilter.getRefreshInterval().pause ||
|
||||
searchSessionManager.hasSearchSessionIdInURL();
|
||||
return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED;
|
||||
|
@ -180,6 +192,7 @@ export function getDataStateContainer({
|
|||
documents$: new BehaviorSubject<DataDocumentsMsg>(initialState),
|
||||
totalHits$: new BehaviorSubject<DataTotalHitsMsg>(initialState),
|
||||
};
|
||||
|
||||
// This is debugging code, helping you to understand which messages are sent to the data observables
|
||||
// Adding a debugger in the functions can be helpful to understand what triggers a message
|
||||
// dataSubjects.main$.subscribe((msg) => addLog('dataSubjects.main$', msg));
|
||||
|
@ -187,11 +200,26 @@ export function getDataStateContainer({
|
|||
// dataSubjects.totalHits$.subscribe((msg) => addLog('dataSubjects.totalHits$', msg););
|
||||
// Add window.ELASTIC_DISCOVER_LOGGER = 'debug' to see messages in console
|
||||
|
||||
let autoRefreshDone: AutoRefreshDoneFn | undefined | null = null;
|
||||
/**
|
||||
* Subscribes to ES|QL fetches to handle state changes when loading or before a fetch completes
|
||||
*/
|
||||
const { esqlFetchSubscribe, cleanupEsql } = buildEsqlFetchSubscribe({
|
||||
internalState,
|
||||
appStateContainer,
|
||||
dataSubjects,
|
||||
injectCurrentTab,
|
||||
});
|
||||
|
||||
// The main subscription to handle state changes
|
||||
dataSubjects.documents$.pipe(switchMap(esqlFetchSubscribe)).subscribe();
|
||||
// Make sure to clean up the ES|QL state when the saved search changes
|
||||
savedSearchContainer.getInitial$().subscribe(cleanupEsql);
|
||||
|
||||
/**
|
||||
* handler emitted by `timefilter.getAutoRefreshFetch$()`
|
||||
* to notify when data completed loading and to start a new autorefresh loop
|
||||
*/
|
||||
let autoRefreshDone: AutoRefreshDoneFn | undefined | null = null;
|
||||
const setAutoRefreshDone = (fn: AutoRefreshDoneFn | undefined) => {
|
||||
autoRefreshDone = fn;
|
||||
};
|
||||
|
@ -200,7 +228,7 @@ export function getDataStateContainer({
|
|||
data,
|
||||
main$: dataSubjects.main$,
|
||||
refetch$,
|
||||
searchSource: getSavedSearch().searchSource,
|
||||
searchSource: savedSearchContainer.getState().searchSource,
|
||||
searchSessionManager,
|
||||
}).pipe(
|
||||
filter(() => validateTimeRange(timefilter.getTime(), toastNotifications)),
|
||||
|
@ -233,7 +261,7 @@ export function getDataStateContainer({
|
|||
services,
|
||||
getAppState: appStateContainer.getState,
|
||||
internalState,
|
||||
savedSearch: getSavedSearch(),
|
||||
savedSearch: savedSearchContainer.getState(),
|
||||
};
|
||||
|
||||
abortController?.abort();
|
||||
|
@ -269,7 +297,7 @@ export function getDataStateContainer({
|
|||
|
||||
await profilesManager.resolveDataSourceProfile({
|
||||
dataSource: appStateContainer.getState().dataSource,
|
||||
dataView: getSavedSearch().searchSource.getField('index'),
|
||||
dataView: savedSearchContainer.getState().searchSource.getField('index'),
|
||||
query: appStateContainer.getState().query,
|
||||
});
|
||||
|
||||
|
@ -359,7 +387,7 @@ export function getDataStateContainer({
|
|||
|
||||
const fetchQuery = async () => {
|
||||
const query = appStateContainer.getState().query;
|
||||
const currentDataView = getSavedSearch().searchSource.getField('index');
|
||||
const currentDataView = savedSearchContainer.getState().searchSource.getField('index');
|
||||
|
||||
if (isOfAggregateQueryType(query)) {
|
||||
const nextDataView = await getEsqlDataView(query, currentDataView, services);
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface DiscoverGlobalStateContainer {
|
|||
set: (state: QueryState) => Promise<void>;
|
||||
}
|
||||
|
||||
const GLOBAL_STATE_URL_KEY = '_g';
|
||||
export const GLOBAL_STATE_URL_KEY = '_g';
|
||||
|
||||
export const getDiscoverGlobalStateContainer = (
|
||||
stateStorage: IKbnUrlStateStorage
|
||||
|
|
|
@ -272,6 +272,7 @@ export function getDiscoverStateContainer({
|
|||
* App State Container, synced with the _a part URL
|
||||
*/
|
||||
const appStateContainer = getDiscoverAppStateContainer({
|
||||
tabId,
|
||||
stateStorage,
|
||||
internalState,
|
||||
savedSearchContainer,
|
||||
|
@ -303,7 +304,7 @@ export function getDiscoverStateContainer({
|
|||
appStateContainer,
|
||||
internalState,
|
||||
runtimeStateManager,
|
||||
getSavedSearch: savedSearchContainer.getState,
|
||||
savedSearchContainer,
|
||||
setDataView,
|
||||
injectCurrentTab,
|
||||
getCurrentTab,
|
||||
|
|
|
@ -108,6 +108,7 @@ export const initializeSession: InternalStateThunkActionCreator<
|
|||
* Session initialization
|
||||
*/
|
||||
|
||||
// TODO: Needs to happen when switching tabs too?
|
||||
if (customizationContext.displayMode === 'standalone' && persistedDiscoverSession) {
|
||||
if (persistedDiscoverSession.id) {
|
||||
services.chrome.recentlyAccessed.add(
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import type { TabbedContentState } from '@kbn/unified-tabs/src/components/tabbed_content/tabbed_content';
|
||||
import { cloneDeep, differenceBy } from 'lodash';
|
||||
import type { QueryState } from '@kbn/data-plugin/common';
|
||||
import type { TabState } from '../types';
|
||||
import { selectAllTabs, selectTab } from '../selectors';
|
||||
import {
|
||||
|
@ -18,6 +19,9 @@ import {
|
|||
type InternalStateThunkActionCreator,
|
||||
} from '../internal_state';
|
||||
import { createTabRuntimeState, selectTabRuntimeState } from '../runtime_state';
|
||||
import { APP_STATE_URL_KEY } from '../../../../../../common';
|
||||
import { GLOBAL_STATE_URL_KEY } from '../../discover_global_state_container';
|
||||
import type { DiscoverAppState } from '../../discover_app_state_container';
|
||||
|
||||
export const setTabs: InternalStateThunkActionCreator<
|
||||
[Parameters<typeof internalStateSlice.actions.setTabs>[0]]
|
||||
|
@ -52,53 +56,60 @@ export const updateTabs: InternalStateThunkActionCreator<[TabbedContentState], P
|
|||
|
||||
if (selectedItem?.id !== currentTab.id) {
|
||||
const previousTabRuntimeState = selectTabRuntimeState(runtimeStateManager, currentTab.id);
|
||||
const previousTabStateContainer = previousTabRuntimeState.stateContainer$.getValue();
|
||||
|
||||
previousTabRuntimeState.stateContainer$.getValue()?.actions.stopSyncing();
|
||||
previousTabStateContainer?.actions.stopSyncing();
|
||||
|
||||
updatedTabs = updatedTabs.map((tab) =>
|
||||
tab.id === currentTab.id
|
||||
? {
|
||||
...tab,
|
||||
globalState: urlStateStorage.get('_g') ?? undefined,
|
||||
appState: urlStateStorage.get('_a') ?? undefined,
|
||||
}
|
||||
: tab
|
||||
);
|
||||
updatedTabs = updatedTabs.map((tab) => {
|
||||
if (tab.id !== currentTab.id) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
const {
|
||||
time: timeRange,
|
||||
refreshInterval,
|
||||
filters,
|
||||
} = previousTabStateContainer?.globalState.get() ?? {};
|
||||
|
||||
return { ...tab, lastPersistedGlobalState: { timeRange, refreshInterval, filters } };
|
||||
});
|
||||
|
||||
const nextTab = selectedItem ? selectTab(currentState, selectedItem.id) : undefined;
|
||||
|
||||
if (nextTab) {
|
||||
await urlStateStorage.set('_g', nextTab.globalState);
|
||||
await urlStateStorage.set('_a', nextTab.appState);
|
||||
} else {
|
||||
await urlStateStorage.set('_g', null);
|
||||
await urlStateStorage.set('_a', null);
|
||||
}
|
||||
|
||||
const nextTabRuntimeState = selectedItem
|
||||
? selectTabRuntimeState(runtimeStateManager, selectedItem.id)
|
||||
: undefined;
|
||||
const nextTabStateContainer = nextTabRuntimeState?.stateContainer$.getValue();
|
||||
|
||||
if (nextTabStateContainer) {
|
||||
if (nextTab && nextTabStateContainer) {
|
||||
const {
|
||||
time,
|
||||
timeRange,
|
||||
refreshInterval,
|
||||
filters: globalFilters,
|
||||
} = nextTabStateContainer.globalState.get() ?? {};
|
||||
const { filters: appFilters, query } = nextTabStateContainer.appState.getState();
|
||||
} = nextTab.lastPersistedGlobalState;
|
||||
const appState = nextTabStateContainer.appState.getState();
|
||||
const { filters: appFilters, query } = appState;
|
||||
|
||||
services.timefilter.setTime(time ?? services.timefilter.getTimeDefaults());
|
||||
await urlStateStorage.set<QueryState>(GLOBAL_STATE_URL_KEY, {
|
||||
time: timeRange,
|
||||
refreshInterval,
|
||||
filters: globalFilters,
|
||||
});
|
||||
await urlStateStorage.set<DiscoverAppState>(APP_STATE_URL_KEY, appState);
|
||||
|
||||
services.timefilter.setTime(timeRange ?? services.timefilter.getTimeDefaults());
|
||||
services.timefilter.setRefreshInterval(
|
||||
refreshInterval ?? services.timefilter.getRefreshIntervalDefaults()
|
||||
);
|
||||
services.filterManager.setGlobalFilters(globalFilters ?? []);
|
||||
services.filterManager.setGlobalFilters(cloneDeep(globalFilters ?? []));
|
||||
services.filterManager.setAppFilters(cloneDeep(appFilters ?? []));
|
||||
services.data.query.queryString.setQuery(
|
||||
query ?? services.data.query.queryString.getDefaultQuery()
|
||||
);
|
||||
|
||||
nextTabStateContainer.actions.initializeAndSync();
|
||||
} else {
|
||||
await urlStateStorage.set(GLOBAL_STATE_URL_KEY, null);
|
||||
await urlStateStorage.set(APP_STATE_URL_KEY, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('InternalStateStore', () => {
|
|||
runtimeStateManager,
|
||||
urlStateStorage: createKbnUrlStateStorage(),
|
||||
});
|
||||
const tabId = store.getState().tabs.allIds[0];
|
||||
const tabId = store.getState().tabs.unsafeCurrentId;
|
||||
expect(selectTab(store.getState(), tabId).dataViewId).toBeUndefined();
|
||||
expect(
|
||||
selectTabRuntimeState(runtimeStateManager, tabId).currentDataView$.value
|
||||
|
|
|
@ -32,6 +32,7 @@ import { selectAllTabs, selectTab } from './selectors';
|
|||
import { createTabItem } from './utils';
|
||||
|
||||
export const defaultTabState: Omit<TabState, keyof TabItem> = {
|
||||
lastPersistedGlobalState: {},
|
||||
dataViewId: undefined,
|
||||
isDataViewLoading: false,
|
||||
dataRequestParams: {},
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { RefreshInterval } from '@kbn/data-plugin/common';
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import type { Filter, TimeRange } from '@kbn/es-query';
|
||||
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
|
||||
import type { TabItem } from '@kbn/unified-tabs';
|
||||
|
||||
|
@ -45,8 +46,11 @@ export interface InternalStateDataRequestParams {
|
|||
}
|
||||
|
||||
export interface TabState extends TabItem {
|
||||
globalState?: Record<string, unknown>;
|
||||
appState?: Record<string, unknown>;
|
||||
lastPersistedGlobalState: {
|
||||
timeRange?: TimeRange;
|
||||
refreshInterval?: RefreshInterval;
|
||||
filters?: Filter[];
|
||||
};
|
||||
dataViewId: string | undefined;
|
||||
isDataViewLoading: boolean;
|
||||
dataRequestParams: InternalStateDataRequestParams;
|
||||
|
|
|
@ -7,29 +7,25 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import type { DataViewsContract } from '@kbn/data-plugin/public';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { useEsqlMode } from './use_esql_mode';
|
||||
import { FetchStatus } from '../../types';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import { savedSearchMock } from '../../../__mocks__/saved_search';
|
||||
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
|
||||
import { DiscoverMainProvider } from '../state_management/discover_state_provider';
|
||||
import type { DiscoverAppState } from '../state_management/discover_app_state_container';
|
||||
import type { DiscoverStateContainer } from '../state_management/discover_state';
|
||||
import { VIEW_MODE } from '@kbn/saved-search-plugin/public';
|
||||
import { dataViewAdHoc } from '../../../__mocks__/data_view_complex';
|
||||
import type { EsHitRecord } from '@kbn/discover-utils';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { omit } from 'lodash';
|
||||
import { CurrentTabProvider, internalStateActions } from '../state_management/redux';
|
||||
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||
import { internalStateActions } from '../redux';
|
||||
import type { DiscoverAppState } from '../discover_app_state_container';
|
||||
import { dataViewAdHoc } from '../../../../__mocks__/data_view_complex';
|
||||
|
||||
async function getHookProps(
|
||||
async function getTestProps(
|
||||
query: AggregateQuery | Query | undefined,
|
||||
dataViewsService: DataViewsContract = discoverServiceMock.dataViews,
|
||||
appState?: Partial<DiscoverAppState>,
|
||||
|
@ -56,6 +52,7 @@ async function getHookProps(
|
|||
replaceUrlState,
|
||||
};
|
||||
}
|
||||
|
||||
const query = { esql: 'from the-data-view-title' };
|
||||
const msgComplete = {
|
||||
fetchStatus: FetchStatus.PARTIAL,
|
||||
|
@ -80,45 +77,35 @@ const getDataViewsService = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const getHookContext = (stateContainer: DiscoverStateContainer) => {
|
||||
return ({ children }: React.PropsWithChildren) => (
|
||||
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
|
||||
<DiscoverMainProvider value={stateContainer}>
|
||||
<>{children}</>
|
||||
</DiscoverMainProvider>
|
||||
</CurrentTabProvider>
|
||||
);
|
||||
};
|
||||
const renderHookWithContext = async (
|
||||
const setupTest = async (
|
||||
useDataViewsService: boolean = false,
|
||||
appState?: DiscoverAppState,
|
||||
defaultFetchStatus?: FetchStatus
|
||||
) => {
|
||||
const props = await getHookProps(
|
||||
const props = await getTestProps(
|
||||
query,
|
||||
useDataViewsService ? getDataViewsService() : undefined,
|
||||
appState,
|
||||
defaultFetchStatus
|
||||
);
|
||||
props.stateContainer.actions.setDataView(dataViewMock);
|
||||
|
||||
renderHook(() => useEsqlMode(props), {
|
||||
wrapper: getHookContext(props.stateContainer),
|
||||
});
|
||||
return props;
|
||||
};
|
||||
|
||||
describe('useEsqlMode', () => {
|
||||
// Testing buildEsqlFetchSubscribe through the state container
|
||||
// since the logic is pretty intertwined with the state management
|
||||
describe('buildEsqlFetchSubscribe', () => {
|
||||
test('an ES|QL query should change state when loading and finished', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(true);
|
||||
const { replaceUrlState, stateContainer } = await setupTest(true);
|
||||
|
||||
replaceUrlState.mockReset();
|
||||
|
||||
stateContainer.dataState.data$.documents$.next(msgComplete);
|
||||
expect(replaceUrlState).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should not change viewMode to undefined (default) if it was AGGREGATED_LEVEL', async () => {
|
||||
const { replaceUrlState } = await renderHookWithContext(false, {
|
||||
const { replaceUrlState } = await setupTest(false, {
|
||||
viewMode: VIEW_MODE.AGGREGATED_LEVEL,
|
||||
});
|
||||
|
||||
|
@ -126,7 +113,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('should change viewMode to undefined (default) if it was PATTERN_LEVEL', async () => {
|
||||
const { replaceUrlState } = await renderHookWithContext(false, {
|
||||
const { replaceUrlState } = await setupTest(false, {
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
});
|
||||
|
||||
|
@ -137,9 +124,9 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('changing an ES|QL query with different result columns should change state when loading and finished', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false);
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
stateContainer.dataState.data$.documents$.next(msgComplete);
|
||||
documents$.next(msgComplete);
|
||||
replaceUrlState.mockReset();
|
||||
|
||||
documents$.next({
|
||||
|
@ -164,9 +151,9 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('changing an ES|QL query with same result columns but a different index pattern should change state when loading and finished', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false);
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
stateContainer.dataState.data$.documents$.next(msgComplete);
|
||||
documents$.next(msgComplete);
|
||||
replaceUrlState.mockReset();
|
||||
|
||||
documents$.next({
|
||||
|
@ -190,9 +177,9 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('changing a ES|QL query with no transformational commands should not change state when loading and finished if index pattern is the same', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false);
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
stateContainer.dataState.data$.documents$.next(msgComplete);
|
||||
documents$.next(msgComplete);
|
||||
await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0));
|
||||
replaceUrlState.mockReset();
|
||||
|
||||
|
@ -231,8 +218,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('only changing an ES|QL query with same result columns should not change columns', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false);
|
||||
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
||||
documents$.next(msgComplete);
|
||||
|
@ -272,8 +258,9 @@ describe('useEsqlMode', () => {
|
|||
|
||||
await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0));
|
||||
});
|
||||
|
||||
test('if its not an ES|QL query coming along, it should be ignored', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false);
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
||||
documents$.next(msgComplete);
|
||||
|
@ -311,7 +298,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('it should not overwrite existing state columns on initial fetch', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false, {
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false, {
|
||||
columns: ['field1'],
|
||||
});
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
@ -355,7 +342,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('it should not overwrite existing state columns on initial fetch and non transformational commands', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false, {
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false, {
|
||||
columns: ['field1'],
|
||||
});
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
@ -375,8 +362,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('it should overwrite existing state columns on transitioning from a query with non transformational commands to a query with transformational', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false, {});
|
||||
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false, {});
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
||||
documents$.next({
|
||||
|
@ -409,7 +395,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('it should not overwrite state column when successfully fetching after an error fetch', async () => {
|
||||
const { replaceUrlState, stateContainer } = await renderHookWithContext(false, {
|
||||
const { replaceUrlState, stateContainer } = await setupTest(false, {
|
||||
columns: [],
|
||||
});
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
@ -469,12 +455,8 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
test('changing an ES|QL query with an index pattern that not corresponds to a dataview should return results', async () => {
|
||||
const props = await getHookProps(query, discoverServiceMock.dataViews);
|
||||
const { stateContainer, replaceUrlState } = props;
|
||||
const { stateContainer, replaceUrlState } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
props.stateContainer.actions.setDataView(dataViewMock);
|
||||
|
||||
renderHook(() => useEsqlMode(props), { wrapper: getHookContext(stateContainer) });
|
||||
|
||||
documents$.next(msgComplete);
|
||||
await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0));
|
||||
|
@ -491,7 +473,7 @@ describe('useEsqlMode', () => {
|
|||
],
|
||||
query: { esql: 'from the-data-view-* | keep field1' },
|
||||
});
|
||||
props.stateContainer.actions.setDataView(dataViewAdHoc);
|
||||
stateContainer.actions.setDataView(dataViewAdHoc);
|
||||
await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1));
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -502,7 +484,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
it('should call setResetDefaultProfileState correctly when index pattern changes', async () => {
|
||||
const { stateContainer } = await renderHookWithContext(
|
||||
const { stateContainer } = await setupTest(
|
||||
false,
|
||||
{ query: { esql: 'from pattern' } },
|
||||
FetchStatus.LOADING
|
||||
|
@ -577,7 +559,7 @@ describe('useEsqlMode', () => {
|
|||
});
|
||||
|
||||
it('should call setResetDefaultProfileState correctly when columns change', async () => {
|
||||
const { stateContainer } = await renderHookWithContext(false);
|
||||
const { stateContainer } = await setupTest(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)];
|
||||
const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)];
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { getIndexPatternFromESQLQuery, hasTransformationalCommand } from '@kbn/esql-utils';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { DataDocumentsMsg, SavedSearchData } from '../discover_data_state_container';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import type { DiscoverAppStateContainer } from '../discover_app_state_container';
|
||||
import type { InternalStateStore, TabActionInjector } from '../redux';
|
||||
import { internalStateActions } from '../redux';
|
||||
import { getValidViewMode } from '../../utils/get_valid_view_mode';
|
||||
|
||||
const ESQL_MAX_NUM_OF_COLUMNS = 50;
|
||||
|
||||
/*
|
||||
* Takes care of ES|QL state transformations when a new result is returned
|
||||
* If necessary this is setting displayed columns and selected data view
|
||||
*/
|
||||
export const buildEsqlFetchSubscribe = ({
|
||||
internalState,
|
||||
appStateContainer,
|
||||
dataSubjects,
|
||||
injectCurrentTab,
|
||||
}: {
|
||||
internalState: InternalStateStore;
|
||||
appStateContainer: DiscoverAppStateContainer;
|
||||
dataSubjects: SavedSearchData;
|
||||
injectCurrentTab: TabActionInjector;
|
||||
}) => {
|
||||
let prevEsqlData: {
|
||||
initialFetch: boolean;
|
||||
query: string;
|
||||
allColumns: string[];
|
||||
defaultColumns: string[];
|
||||
} = {
|
||||
initialFetch: true,
|
||||
query: '',
|
||||
allColumns: [],
|
||||
defaultColumns: [],
|
||||
};
|
||||
|
||||
const cleanupEsql = () => {
|
||||
if (!prevEsqlData.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cleanup when it's not an ES|QL query
|
||||
prevEsqlData = {
|
||||
initialFetch: true,
|
||||
query: '',
|
||||
allColumns: [],
|
||||
defaultColumns: [],
|
||||
};
|
||||
};
|
||||
|
||||
const esqlFetchSubscribe = async (next: DataDocumentsMsg) => {
|
||||
const { query: nextQuery } = next;
|
||||
|
||||
if (!nextQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOfAggregateQueryType(nextQuery)) {
|
||||
// cleanup for a "regular" query
|
||||
cleanupEsql();
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to reset the default profile state on index pattern changes
|
||||
// when loading starts to ensure the correct pre fetch state is available
|
||||
// before data fetching is triggered
|
||||
if (next.fetchStatus === FetchStatus.LOADING) {
|
||||
// We have to grab the current query from appState
|
||||
// here since nextQuery has not been updated yet
|
||||
const appStateQuery = appStateContainer.getState().query;
|
||||
|
||||
if (isOfAggregateQueryType(appStateQuery)) {
|
||||
if (prevEsqlData.initialFetch) {
|
||||
prevEsqlData.query = appStateQuery.esql;
|
||||
}
|
||||
|
||||
const indexPatternChanged =
|
||||
getIndexPatternFromESQLQuery(appStateQuery.esql) !==
|
||||
getIndexPatternFromESQLQuery(prevEsqlData.query);
|
||||
|
||||
// Reset all default profile state when index pattern changes
|
||||
if (indexPatternChanged) {
|
||||
internalState.dispatch(
|
||||
injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
|
||||
resetDefaultProfileState: {
|
||||
columns: true,
|
||||
rowHeight: true,
|
||||
breakdownField: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (next.fetchStatus === FetchStatus.ERROR) {
|
||||
// An error occurred, but it's still considered an initial fetch
|
||||
prevEsqlData.initialFetch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (next.fetchStatus !== FetchStatus.PARTIAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextAllColumns = prevEsqlData.allColumns;
|
||||
let nextDefaultColumns = prevEsqlData.defaultColumns;
|
||||
|
||||
if (next.result?.length) {
|
||||
nextAllColumns = Object.keys(next.result[0].raw);
|
||||
|
||||
if (hasTransformationalCommand(nextQuery.esql)) {
|
||||
nextDefaultColumns = nextAllColumns.slice(0, ESQL_MAX_NUM_OF_COLUMNS);
|
||||
} else {
|
||||
nextDefaultColumns = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (prevEsqlData.initialFetch) {
|
||||
prevEsqlData.initialFetch = false;
|
||||
prevEsqlData.query = nextQuery.esql;
|
||||
prevEsqlData.allColumns = nextAllColumns;
|
||||
prevEsqlData.defaultColumns = nextDefaultColumns;
|
||||
}
|
||||
|
||||
const indexPatternChanged =
|
||||
getIndexPatternFromESQLQuery(nextQuery.esql) !==
|
||||
getIndexPatternFromESQLQuery(prevEsqlData.query);
|
||||
|
||||
const allColumnsChanged = !isEqual(nextAllColumns, prevEsqlData.allColumns);
|
||||
|
||||
const changeDefaultColumns =
|
||||
indexPatternChanged || !isEqual(nextDefaultColumns, prevEsqlData.defaultColumns);
|
||||
|
||||
const { viewMode } = appStateContainer.getState();
|
||||
const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true });
|
||||
|
||||
// If the index pattern hasn't changed, but the available columns have changed
|
||||
// due to transformational commands, reset the associated default profile state
|
||||
if (!indexPatternChanged && allColumnsChanged) {
|
||||
internalState.dispatch(
|
||||
injectCurrentTab(internalStateActions.setResetDefaultProfileState)({
|
||||
resetDefaultProfileState: {
|
||||
columns: true,
|
||||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prevEsqlData.allColumns = nextAllColumns;
|
||||
|
||||
if (indexPatternChanged || changeDefaultColumns || changeViewMode) {
|
||||
prevEsqlData.query = nextQuery.esql;
|
||||
prevEsqlData.defaultColumns = nextDefaultColumns;
|
||||
|
||||
// just change URL state if necessary
|
||||
if (changeDefaultColumns || changeViewMode) {
|
||||
const nextState = {
|
||||
...(changeDefaultColumns && { columns: nextDefaultColumns }),
|
||||
...(changeViewMode && { viewMode: undefined }),
|
||||
};
|
||||
|
||||
await appStateContainer.replaceUrlState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
dataSubjects.documents$.next({
|
||||
...next,
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
});
|
||||
};
|
||||
|
||||
return { esqlFetchSubscribe, cleanupEsql };
|
||||
};
|
|
@ -10,3 +10,6 @@
|
|||
export const ADHOC_DATA_VIEW_RENDER_EVENT = 'ad_hoc_data_view';
|
||||
|
||||
export const SEARCH_SESSION_ID_QUERY_PARAM = 'searchSessionId';
|
||||
|
||||
// TEMPORARY: This is a temporary flag to enable/disable tabs in Discover until the feature is fully implemented.
|
||||
export const TABS_ENABLED = false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue