[Discover] Refactor default appState generation (#140572)

This commit is contained in:
Matthias Wilhelm 2022-09-19 08:56:15 +02:00 committed by GitHub
parent 2c4e1fb547
commit d0a43c9688
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 117 additions and 154 deletions

View file

@ -46,6 +46,7 @@ const fields = [
scripted: false,
filterable: true,
aggregatable: true,
sortable: true,
},
{
name: 'scripted',

View file

@ -20,3 +20,11 @@ export const savedSearchMockWithTimeField = {
id: 'the-saved-search-id-with-timefield',
searchSource: createSearchSourceMock({ index: dataViewWithTimefieldMock }),
} as unknown as SavedSearch;
export const savedSearchMockWithSQL = {
id: 'the-saved-search-id-sql',
searchSource: createSearchSourceMock({
index: dataViewWithTimefieldMock,
query: { sql: 'SELECT * FROM "the-saved-search-id-sql"' },
}),
} as unknown as SavedSearch;

View file

@ -63,7 +63,7 @@ export const discoverServiceMock = {
if (key === 'fields:popularLimit') {
return 5;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return [];
return ['default_column'];
} else if (key === UI_SETTINGS.META_FIELDS) {
return [];
} else if (key === DOC_HIDE_TIME_COLUMN_SETTING) {

View file

@ -53,7 +53,7 @@ async function saveDataSource({
navigateTo(`/view/${encodeURIComponent(id)}`);
} else {
// Update defaults so that "reload saved query" functions correctly
state.resetAppState();
state.resetAppState(savedSearch);
services.chrome.docTitle.change(savedSearch.title!);
setBreadcrumbsTitle(

View file

@ -8,7 +8,6 @@
import React, { useEffect, useState, memo, useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { DataViewListItem } from '@kbn/data-plugin/public';
import { ISearchSource } from '@kbn/data-plugin/public';
import { DataViewSavedObjectConflictError } from '@kbn/data-views-plugin/public';
import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
@ -70,7 +69,7 @@ export function DiscoverMainRoute(props: Props) {
});
const loadDefaultOrCurrentDataView = useCallback(
async (searchSource: ISearchSource) => {
async (nextSavedSearch: SavedSearch) => {
try {
const hasUserDataViewValue = await data.dataViews.hasData
.hasUserDataView()
@ -92,12 +91,12 @@ export function DiscoverMainRoute(props: Props) {
return;
}
const { appStateContainer } = getState({ history, uiSettings: config });
const { appStateContainer } = getState({ history, savedSearch: nextSavedSearch, services });
const { index } = appStateContainer.getState();
const ip = await loadDataView(data.dataViews, config, index);
const ipList = ip.list;
const dataViewData = resolveDataView(ip, searchSource, toastNotifications);
const dataViewData = resolveDataView(ip, nextSavedSearch.searchSource, toastNotifications);
await data.dataViews.refreshFields(dataViewData);
setDataViewList(ipList);
@ -106,7 +105,7 @@ export function DiscoverMainRoute(props: Props) {
setError(e);
}
},
[config, data.dataViews, history, isDev, toastNotifications]
[config, data.dataViews, history, isDev, toastNotifications, services]
);
const loadSavedSearch = useCallback(async () => {
@ -118,7 +117,7 @@ export function DiscoverMainRoute(props: Props) {
savedObjectsTagging: services.savedObjectsTagging,
});
const currentDataView = await loadDefaultOrCurrentDataView(currentSavedSearch.searchSource);
const currentDataView = await loadDefaultOrCurrentDataView(currentSavedSearch);
if (!currentDataView) {
return;

View file

@ -44,7 +44,7 @@ export function useDiscoverState({
setExpandedDoc: (doc?: DataTableRecord) => void;
dataViewList: DataViewListItem[];
}) {
const { uiSettings, data, filterManager, dataViews, storage } = services;
const { uiSettings, data, filterManager, dataViews } = services;
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const { timefilter } = data.query.timefilter;
@ -60,19 +60,11 @@ export function useDiscoverState({
const stateContainer = useMemo(
() =>
getState({
getStateDefaults: () =>
getStateDefaults({
config: uiSettings,
data,
savedSearch,
storage,
}),
storeInSessionStorage: uiSettings.get('state:storeInSessionStorage'),
history,
toasts: services.core.notifications.toasts,
uiSettings,
savedSearch,
services,
}),
[uiSettings, data, history, savedSearch, services.core.notifications.toasts, storage]
[history, savedSearch, services]
);
const { appStateContainer } = stateContainer;
@ -231,10 +223,8 @@ export function useDiscoverState({
const newDataView = newSavedSearch.searchSource.getField('index') || dataView;
newSavedSearch.searchSource.setField('index', newDataView);
const newAppState = getStateDefaults({
config: uiSettings,
data,
savedSearch: newSavedSearch,
storage,
services,
});
restoreStateFromSavedSearch({
@ -245,7 +235,7 @@ export function useDiscoverState({
await stateContainer.replaceUrlAppState(newAppState);
setState(newAppState);
},
[services, dataView, uiSettings, data, storage, stateContainer]
[services, dataView, stateContainer]
);
/**

View file

@ -9,10 +9,9 @@ import { Subject } from 'rxjs';
import { renderHook } from '@testing-library/react-hooks';
import { createSearchSessionMock } from '../../../__mocks__/search_session';
import { discoverServiceMock } from '../../../__mocks__/services';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { savedSearchMock, savedSearchMockWithSQL } from '../../../__mocks__/saved_search';
import { RecordRawType, useSavedSearch } from './use_saved_search';
import { getState, AppState } from '../services/discover_state';
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
import { getState } from '../services/discover_state';
import { useDiscoverState } from './use_discover_state';
import { FetchStatus } from '../../types';
import { dataViewMock } from '../../../__mocks__/data_view';
@ -25,9 +24,9 @@ describe('test useSavedSearch', () => {
test('useSavedSearch return is valid', async () => {
const { history, searchSessionManager } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () => ({ index: 'the-data-view-id' }),
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
const { result } = renderHook(() => {
@ -51,9 +50,9 @@ describe('test useSavedSearch', () => {
test('refetch$ triggers a search', async () => {
const { history, searchSessionManager } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () => ({ index: 'the-data-view-id' }),
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
@ -95,9 +94,9 @@ describe('test useSavedSearch', () => {
test('reset sets back to initial state', async () => {
const { history, searchSessionManager } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () => ({ index: 'the-data-view-id' }),
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
@ -139,21 +138,17 @@ describe('test useSavedSearch', () => {
test('useSavedSearch returns plain record raw type', async () => {
const { history, searchSessionManager } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () =>
({
index: 'the-index-pattern-id',
query: { sql: 'SELECT * FROM test' },
} as unknown as AppState),
savedSearch: savedSearchMockWithSQL,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
const { result } = renderHook(() => {
return useSavedSearch({
initialFetchStatus: FetchStatus.LOADING,
savedSearch: savedSearchMock,
savedSearch: savedSearchMockWithSQL,
searchSessionManager,
searchSource: savedSearchMock.searchSource.createCopy(),
searchSource: savedSearchMockWithSQL.searchSource.createCopy(),
services: discoverServiceMock,
stateContainer,
useNewFieldsApi: true,

View file

@ -12,15 +12,14 @@ import { createSearchSessionMock } from '../../../__mocks__/search_session';
import { discoverServiceMock } from '../../../__mocks__/services';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { getState } from '../services/discover_state';
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
describe('test useSearchSession', () => {
test('getting the next session id', async () => {
const { history } = createSearchSessionMock();
const stateContainer = getState({
getStateDefaults: () => ({ index: 'test' }),
savedSearch: savedSearchMock,
history,
uiSettings: uiSettingsMock,
services: discoverServiceMock,
});
const nextId = 'id';

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { IUiSettingsClient } from '@kbn/core/public';
import {
getState,
GetStateReturn,
@ -14,17 +13,14 @@ import {
} from './discover_state';
import { createBrowserHistory, History } from 'history';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search';
import { discoverServiceMock } from '../../../__mocks__/services';
let history: History;
let state: GetStateReturn;
const getCurrentUrl = () => history.createHref(history.location);
const uiSettingsMock = {
get: <T>(key: string) => (key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown as T,
} as IUiSettingsClient;
describe('Test discover state', () => {
let stopSync = () => {};
@ -32,9 +28,9 @@ describe('Test discover state', () => {
history = createBrowserHistory();
history.push('/');
state = getState({
getStateDefaults: () => ({ index: 'test' }),
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
await state.replaceUrlAppState({});
stopSync = state.startSync();
@ -46,7 +42,9 @@ describe('Test discover state', () => {
test('setting app state and syncing to URL', async () => {
state.setAppState({ index: 'modified' });
state.flushToUrl();
expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_a=(index:modified)"`);
expect(getCurrentUrl()).toMatchInlineSnapshot(
`"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"`
);
});
test('changing URL to be propagated to appState', async () => {
@ -60,11 +58,9 @@ describe('Test discover state', () => {
test('URL navigation to url without _a, state should not change', async () => {
history.push('/#?_a=(index:modified)');
history.push('/');
expect(state.appStateContainer.getState()).toMatchInlineSnapshot(`
Object {
"index": "modified",
}
`);
expect(state.appStateContainer.getState()).toEqual({
index: 'modified',
});
});
test('isAppStateDirty returns whether the current state has changed', async () => {
@ -89,46 +85,45 @@ describe('Test discover state', () => {
});
});
describe('Test discover initial state sort handling', () => {
test('Non-empty sort in URL should not fallback to state defaults', async () => {
test('Non-empty sort in URL should not be overwritten by saved search sort', async () => {
history = createBrowserHistory();
history.push('/#?_a=(sort:!(!(order_date,desc)))');
state = getState({
getStateDefaults: () => ({ sort: [['fallback', 'desc']] }),
savedSearch: { ...savedSearchMock, ...{ sort: [['bytes', 'desc']] } },
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
await state.replaceUrlAppState({});
const stopSync = state.startSync();
expect(state.appStateContainer.getState().sort).toMatchInlineSnapshot(`
Array [
Array [
"order_date",
"desc",
],
]
`);
expect(state.appStateContainer.getState().sort).toEqual([['order_date', 'desc']]);
stopSync();
});
test('Empty sort in URL should allow fallback state defaults', async () => {
test('Empty sort in URL should use saved search sort for state', async () => {
history = createBrowserHistory();
history.push('/#?_a=(sort:!())');
const nextSavedSearch = { ...savedSearchMock, ...{ sort: [['bytes', 'desc']] as SortOrder[] } };
state = getState({
getStateDefaults: () => ({ sort: [['fallback', 'desc']] }),
savedSearch: nextSavedSearch,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
await state.replaceUrlAppState({});
const stopSync = state.startSync();
expect(state.appStateContainer.getState().sort).toMatchInlineSnapshot(`
Array [
Array [
"fallback",
"desc",
],
]
`);
expect(state.appStateContainer.getState().sort).toEqual([['bytes', 'desc']]);
stopSync();
});
test('Empty sort in URL and saved search should sort by timestamp', async () => {
history = createBrowserHistory();
history.push('/#?_a=(sort:!())');
state = getState({
savedSearch: savedSearchMockWithTimeField,
services: discoverServiceMock,
history,
});
await state.replaceUrlAppState({});
const stopSync = state.startSync();
expect(state.appStateContainer.getState().sort).toEqual([['timestamp', 'desc']]);
stopSync();
});
});
@ -140,14 +135,12 @@ describe('Test discover state with legacy migration', () => {
"/#?_a=(query:(query_string:(analyze_wildcard:!t,query:'type:nice%20name:%22yeah%22')))"
);
state = getState({
getStateDefaults: () => ({ index: 'test' }),
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
uiSettings: uiSettingsMock,
});
expect(state.appStateContainer.getState()).toMatchInlineSnapshot(`
expect(state.appStateContainer.getState().query).toMatchInlineSnapshot(`
Object {
"index": "test",
"query": Object {
"language": "lucene",
"query": Object {
"query_string": Object {
@ -155,7 +148,6 @@ describe('Test discover state with legacy migration', () => {
"query": "type:nice name:\\"yeah\\"",
},
},
},
}
`);
});
@ -167,8 +159,9 @@ describe('createSearchSessionRestorationDataProvider', () => {
const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({
data: mockDataPlugin,
appStateContainer: getState({
history: createBrowserHistory(),
uiSettings: uiSettingsMock,
savedSearch: savedSearchMock,
services: discoverServiceMock,
history,
}).appStateContainer,
getSavedSearch: () => mockSavedSearch,
});
@ -217,12 +210,10 @@ describe('createSearchSessionRestorationDataProvider', () => {
test('restoreState has paused autoRefresh', async () => {
const { initialState, restoreState } = await searchSessionInfoProvider.getLocatorData();
expect(initialState.refreshInterval).toBe(undefined);
expect(restoreState.refreshInterval).toMatchInlineSnapshot(`
Object {
"pause": true,
"value": 0,
}
`);
expect(restoreState.refreshInterval).toEqual({
pause: true,
value: 0,
});
});
});
});

View file

@ -9,7 +9,6 @@
import { cloneDeep, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import { NotificationsStart, IUiSettingsClient } from '@kbn/core/public';
import {
Filter,
FilterStateStore,
@ -37,6 +36,8 @@ import {
} from '@kbn/data-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { getStateDefaults } from '../utils/get_state_defaults';
import { DiscoverServices } from '../../../build_services';
import { DiscoverGridSettings } from '../../../components/discover_grid/types';
import { handleSourceColumnState } from '../../../utils/state_helpers';
import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '../../../locator';
@ -107,30 +108,18 @@ export interface AppStateUrl extends Omit<AppState, 'sort'> {
}
interface GetStateParams {
/**
* Default state used for merging with with URL state to get the initial state
*/
getStateDefaults?: () => AppState;
/**
* Determins the use of long vs. short/hashed urls
*/
storeInSessionStorage?: boolean;
/**
* Browser history
*/
history: History;
/**
* Core's notifications.toasts service
* In case it is passed in,
* kbnUrlStateStorage will use it notifying about inner errors
* The current savedSearch
*/
toasts?: NotificationsStart['toasts'];
savedSearch: SavedSearch;
/**
* core ui settings service
*/
uiSettings: IUiSettingsClient;
services: DiscoverServices;
}
export interface GetStateReturn {
@ -179,9 +168,9 @@ export interface GetStateReturn {
*/
isAppStateDirty: () => boolean;
/**
* Reset AppState to default, discarding all changes
* Reset AppState by the given savedSearch discarding all changes
*/
resetAppState: () => void;
resetAppState: (nextSavedSearch: SavedSearch) => void;
/**
* Pause the auto refresh interval without pushing an entry to history
*/
@ -195,14 +184,13 @@ const GLOBAL_STATE_URL_KEY = '_g';
* Builds and returns appState and globalState containers and helper functions
* Used to sync URL with UI state
*/
export function getState({
getStateDefaults,
storeInSessionStorage = false,
history,
toasts,
uiSettings,
}: GetStateParams): GetStateReturn {
const defaultAppState = getStateDefaults ? getStateDefaults() : {};
export function getState({ history, savedSearch, services }: GetStateParams): GetStateReturn {
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
const toasts = services.core.notifications.toasts;
const defaultAppState = getStateDefaults({
savedSearch,
services,
});
const stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
@ -216,7 +204,7 @@ export function getState({
...defaultAppState,
...appStateFromUrl,
},
uiSettings
services.uiSettings
);
// todo filter source depending on fields fetching flag (if no columns remain and source fetching is enabled, use default columns)
@ -275,10 +263,10 @@ export function getState({
resetInitialAppState: () => {
initialAppState = appStateContainer.getState();
},
resetAppState: () => {
resetAppState: (nextSavedSearch: SavedSearch) => {
const defaultState = handleSourceColumnState(
getStateDefaults ? getStateDefaults() : {},
uiSettings
getStateDefaults({ savedSearch: nextSavedSearch, services }),
services.uiSettings
);
setState(appStateContainerModified, defaultState);
},

View file

@ -7,23 +7,18 @@
*/
import { getStateDefaults } from './get_state_defaults';
import { createSearchSourceMock, dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { dataViewMock } from '../../../__mocks__/data_view';
import { discoverServiceMock } from '../../../__mocks__/services';
describe('getStateDefaults', () => {
const storage = discoverServiceMock.storage;
test('data view with timefield', () => {
savedSearchMock.searchSource = createSearchSourceMock({ index: dataViewWithTimefieldMock });
const actual = getStateDefaults({
config: uiSettingsMock,
data: dataPluginMock.createStartContract(),
services: discoverServiceMock,
savedSearch: savedSearchMock,
storage,
});
expect(actual).toMatchInlineSnapshot(`
Object {
@ -55,10 +50,8 @@ describe('getStateDefaults', () => {
savedSearchMock.searchSource = createSearchSourceMock({ index: dataViewMock });
const actual = getStateDefaults({
config: uiSettingsMock,
data: dataPluginMock.createStartContract(),
services: discoverServiceMock,
savedSearch: savedSearchMock,
storage,
});
expect(actual).toMatchInlineSnapshot(`
Object {

View file

@ -8,9 +8,8 @@
import { cloneDeep, isEqual } from 'lodash';
import { IUiSettingsClient } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { DiscoverServices } from '../../../build_services';
import { getDefaultSort, getSortArray } from '../../../utils/sorting';
import {
DEFAULT_COLUMNS_SETTING,
@ -22,33 +21,33 @@ import {
import { AppState } from '../services/discover_state';
import { CHART_HIDDEN_KEY } from '../components/chart/discover_chart';
function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) {
function getDefaultColumns(savedSearch: SavedSearch, uiSettings: IUiSettingsClient) {
if (savedSearch.columns && savedSearch.columns.length > 0) {
return [...savedSearch.columns];
}
if (config.get(SEARCH_FIELDS_FROM_SOURCE) && isEqual(config.get(DEFAULT_COLUMNS_SETTING), [])) {
if (
uiSettings.get(SEARCH_FIELDS_FROM_SOURCE) &&
isEqual(uiSettings.get(DEFAULT_COLUMNS_SETTING), [])
) {
return ['_source'];
}
return [...config.get(DEFAULT_COLUMNS_SETTING)];
return [...uiSettings.get(DEFAULT_COLUMNS_SETTING)];
}
export function getStateDefaults({
config,
data,
savedSearch,
storage,
services,
}: {
config: IUiSettingsClient;
data: DataPublicPluginStart;
savedSearch: SavedSearch;
storage: Storage;
services: DiscoverServices;
}) {
const { searchSource } = savedSearch;
const { data, uiSettings, storage } = services;
const dataView = searchSource.getField('index');
const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery();
const sort = getSortArray(savedSearch.sort ?? [], dataView!);
const columns = getDefaultColumns(savedSearch, config);
const columns = getDefaultColumns(savedSearch, uiSettings);
const chartHidden = storage.get(CHART_HIDDEN_KEY);
const defaultState: AppState = {
@ -56,8 +55,8 @@ export function getStateDefaults({
sort: !sort.length
? getDefaultSort(
dataView,
config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'),
config.get(DOC_HIDE_TIME_COLUMN_SETTING, false)
uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'),
uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false)
)
: sort,
columns,