[Discover Tabs] Restore sidebar width and collapsible state when switching tabs (#225327)

Follow up for 
- https://github.com/elastic/kibana/pull/224299

## Summary

- [x] sidebar width
- [x] is sidebar collapsed
- [x] add functional tests 


### Checklist

- [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
- [x] [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)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2025-06-27 09:19:50 +02:00 committed by GitHub
parent 170670cfa1
commit d0da9f94c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 451 additions and 30 deletions

View file

@ -91,6 +91,7 @@ enabled:
- src/platform/test/functional/apps/discover/group10/config.ts
- src/platform/test/functional/apps/discover/context_awareness/config.ts
- src/platform/test/functional/apps/discover/observability/config.ts
- src/platform/test/functional/apps/discover/tabs/config.ts
- src/platform/test/functional/apps/getting_started/config.ts
- src/platform/test/functional/apps/home/config.ts
- src/platform/test/functional/apps/kibana_overview/config.ts

View file

@ -22,6 +22,7 @@ import React, {
} from 'react';
import useLatest from 'react-use/lib/useLatest';
import useUnmount from 'react-use/lib/useUnmount';
import useMount from 'react-use/lib/useMount';
import { BehaviorSubject, Subject, map } from 'rxjs';
export interface RestorableStateProviderProps<TState extends object> {
@ -168,8 +169,12 @@ export const createRestorableStateProvider = <TState extends object>() => {
const useRestorableState = <TKey extends keyof TState>(
key: TKey,
initialValue: InitialValue<TState, TKey>,
shouldIgnoredRestoredValue?: ShouldIgnoredRestoredValue<TState, TKey>
options?: {
shouldIgnoredRestoredValue?: ShouldIgnoredRestoredValue<TState, TKey>;
shouldStoreDefaultValueRightAway?: boolean;
}
) => {
const { shouldIgnoredRestoredValue, shouldStoreDefaultValueRightAway } = options || {};
const { initialState$, onInitialStateChange } = useContext(context);
const [value, _setValue] = useState(() =>
getInitialValue(initialState$.getValue(), key, initialValue, shouldIgnoredRestoredValue)
@ -189,16 +194,38 @@ export const createRestorableStateProvider = <TState extends object>() => {
});
});
useMount(() => {
const restorableState = initialState$.getValue();
if (shouldStoreDefaultValueRightAway && value !== restorableState?.[key]) {
onInitialStateChange?.({ ...restorableState, [key]: value });
}
});
useInitialStateRefresh(key, initialValue, _setValue, shouldIgnoredRestoredValue);
return [value, setValue] as const;
};
const useRestorableRef = <TKey extends keyof TState>(key: TKey, initialValue: TState[TKey]) => {
const useRestorableRef = <TKey extends keyof TState>(
key: TKey,
initialValue: TState[TKey],
options?: {
shouldStoreDefaultValueRightAway?: boolean;
}
) => {
const { shouldStoreDefaultValueRightAway } = options || {};
const { initialState$, onInitialStateChange } = useContext(context);
const initialState = initialState$.getValue();
const valueRef = useRef(getInitialValue(initialState, key, initialValue));
useMount(() => {
const value = valueRef.current;
const restorableState = initialState$.getValue();
if (shouldStoreDefaultValueRightAway && value !== restorableState?.[key]) {
onInitialStateChange?.({ ...restorableState, [key]: value });
}
});
useUnmount(() => {
onInitialStateChange?.({ ...initialState$.getValue(), [key]: valueRef.current });
});

View file

@ -93,11 +93,14 @@ function InnerFieldListGrouped<T extends FieldListItem = DataViewField>({
];
})
),
(restoredAccordionState) => {
return (
fieldGroupsToShow.length !== Object.keys(restoredAccordionState).length ||
fieldGroupsToShow.some(([key]) => !(key in restoredAccordionState))
);
{
shouldStoreDefaultValueRightAway: true, // otherwise, it would re-initialize with the localStorage value which might get updated in the meantime
shouldIgnoredRestoredValue: (restoredAccordionState) => {
return (
fieldGroupsToShow.length !== Object.keys(restoredAccordionState).length ||
fieldGroupsToShow.some(([key]) => !(key in restoredAccordionState))
);
},
}
);

View file

@ -183,9 +183,13 @@ const UnifiedFieldListSidebarContainer = forwardRef<
localStorageKey: stateService.creationOptions.localStorageKeyPrefix
? `${stateService.creationOptions.localStorageKeyPrefix}:sidebarClosed`
: undefined,
isInitiallyCollapsed: initialState?.isCollapsed,
})
);
const isSidebarCollapsed = useObservable(sidebarVisibility.isCollapsed$, false);
const isSidebarCollapsed = useObservable(
sidebarVisibility.isCollapsed$,
sidebarVisibility.initialValue
);
const canEditDataView =
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||

View file

@ -21,6 +21,7 @@ describe('UnifiedFieldList getSidebarVisibility', () => {
const state = getSidebarVisibility({ localStorageKey });
expect(state.isCollapsed$.getValue()).toBe(false);
expect(state.initialValue).toBe(false);
act(() => {
state.toggle(true);
@ -43,6 +44,7 @@ describe('UnifiedFieldList getSidebarVisibility', () => {
const state = getSidebarVisibility({ localStorageKey });
expect(state.isCollapsed$.getValue()).toBe(true);
expect(state.initialValue).toBe(true);
act(() => {
state.toggle(false);
@ -52,6 +54,15 @@ describe('UnifiedFieldList getSidebarVisibility', () => {
expect(localStorage.getItem(localStorageKey)).toBe('false');
});
it('should ignore local storage value if initial value is specified', async () => {
localStorage.setItem(localStorageKey, 'true');
const state = getSidebarVisibility({ localStorageKey, isInitiallyCollapsed: false });
expect(state.isCollapsed$.getValue()).toBe(false);
expect(state.initialValue).toBe(false);
});
it('should not persist if local storage key is not defined', async () => {
const state = getSidebarVisibility({ localStorageKey: undefined });

View file

@ -12,24 +12,33 @@ import { BehaviorSubject } from 'rxjs';
export interface SidebarVisibility {
isCollapsed$: BehaviorSubject<boolean>;
toggle: (isCollapsed: boolean) => void;
initialValue: boolean;
}
export interface GetSidebarStateParams {
localStorageKey?: string;
isInitiallyCollapsed?: boolean;
}
/**
* For managing sidebar visibility state
* @param localStorageKey
* @param isInitiallyCollapsed
*/
export const getSidebarVisibility = ({
localStorageKey,
isInitiallyCollapsed,
}: GetSidebarStateParams): SidebarVisibility => {
const isCollapsed$ = new BehaviorSubject<boolean>(
localStorageKey ? getIsCollapsed(localStorageKey) : false
);
const isCollapsedBasedOnLocalStorage = localStorageKey ? getIsCollapsed(localStorageKey) : false;
const initialValue =
typeof isInitiallyCollapsed === 'boolean'
? isInitiallyCollapsed
: isCollapsedBasedOnLocalStorage;
const isCollapsed$ = new BehaviorSubject<boolean>(initialValue);
return {
initialValue,
isCollapsed$,
toggle: (isCollapsed) => {
isCollapsed$.next(isCollapsed);

View file

@ -7,9 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar';
import { useRestorableRef } from '../../../restorable_state';
/**
* Toggle button props
@ -37,6 +38,14 @@ export const SidebarToggleButton: React.FC<SidebarToggleButtonProps> = ({
buttonSize,
onChange,
}) => {
const isSidebarCollapsedRef = useRestorableRef('isCollapsed', isSidebarCollapsed, {
shouldStoreDefaultValueRightAway: true, // otherwise, it would re-initialize with the localStorage value which might
});
useEffect(() => {
isSidebarCollapsedRef.current = isSidebarCollapsed;
}, [isSidebarCollapsed, isSidebarCollapsedRef]);
return (
<div data-test-subj={dataTestSubj}>
<IconButtonGroup

View file

@ -11,6 +11,10 @@ import { createRestorableStateProvider } from '@kbn/restorable-state';
import type { FieldTypeKnown } from '@kbn/field-utils';
export interface UnifiedFieldListRestorableState {
/**
* Whether the field list is collapsed or expanded
*/
isCollapsed: boolean;
/**
* Field search
*/

View file

@ -59,9 +59,16 @@ import type { PanelsToggleProps } from '../../../../components/panels_toggle';
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 {
internalStateActions,
useCurrentDataView,
useCurrentTabAction,
useCurrentTabSelector,
useInternalStateDispatch,
} from '../../state_management/redux';
import { TABS_ENABLED } from '../../../../constants';
import { DiscoverHistogramLayout } from './discover_histogram_layout';
import type { DiscoverLayoutRestorableState } from './discover_layout_restorable_state';
import { useScopedServices } from '../../../../components/scoped_services_provider';
const queryClient = new QueryClient();
@ -377,6 +384,20 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sendErrorMsg(stateContainer.dataState.data$.main$);
}, [stateContainer.dataState]);
const dispatch = useInternalStateDispatch();
const layoutUiState = useCurrentTabSelector((state) => state.uiState.layout);
const setLayoutUiState = useCurrentTabAction(internalStateActions.setLayoutUiState);
const onInitialStateChange = useCallback(
(newLayoutUiState: Partial<DiscoverLayoutRestorableState>) => {
dispatch(
setLayoutUiState({
layoutUiState: newLayoutUiState,
})
);
},
[dispatch, setLayoutUiState]
);
return (
<EuiPage
className="dscPage" // class is used in tests and other styles
@ -497,6 +518,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
)}
</div>
}
initialState={layoutUiState}
onInitialStateChange={onInitialStateChange}
/>
</div>
</EuiPageBody>

View file

@ -0,0 +1,17 @@
/*
* 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 { createRestorableStateProvider } from '@kbn/restorable-state';
export interface DiscoverLayoutRestorableState {
sidebarWidth: number;
}
export const { withRestorableState, useRestorableState } =
createRestorableStateProvider<DiscoverLayoutRestorableState>();

View file

@ -16,20 +16,25 @@ import { findTestSubject } from '@kbn/test-jest-helpers';
import { mount } from 'enzyme';
import { isEqual as mockIsEqual } from 'lodash';
import React from 'react';
import { DiscoverResizableLayout, SIDEBAR_WIDTH_KEY } from './discover_resizable_layout';
import {
DiscoverResizableLayout as OriginalDiscoverResizableLayout,
type DiscoverResizableLayoutProps,
SIDEBAR_WIDTH_KEY,
} from './discover_resizable_layout';
import { BehaviorSubject } from 'rxjs';
import type { SidebarToggleState } from '../../../types';
import { DiscoverTestProvider } from '../../../../__mocks__/test_provider';
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
const mockSidebarKey = SIDEBAR_WIDTH_KEY;
let mockSidebarWidth: number | undefined;
jest.mock('react-use/lib/useLocalStorage', () => {
return jest.fn((key: string, initialValue: number) => {
if (key !== mockSidebarKey) {
throw new Error(`Unexpected key: ${key}`);
}
return [mockSidebarWidth ?? initialValue, jest.fn()];
});
const services = createDiscoverServicesMock();
services.storage.get = jest.fn((key: string) => {
if (key === mockSidebarKey) {
return mockSidebarWidth;
}
throw new Error(`Unexpected key: ${key}`);
});
let mockIsMobile = false;
@ -47,6 +52,14 @@ jest.mock('@elastic/eui', () => {
};
});
const DiscoverResizableLayout: React.FC<DiscoverResizableLayoutProps> = (props) => {
return (
<DiscoverTestProvider services={services}>
<OriginalDiscoverResizableLayout {...props} />
</DiscoverTestProvider>
);
};
describe('DiscoverResizableLayout', () => {
beforeEach(() => {
mockSidebarWidth = undefined;
@ -106,6 +119,27 @@ describe('DiscoverResizableLayout', () => {
expect(wrapper.find(ResizableLayout).prop('fixedPanelSize')).toBe(400);
});
it('should use the restored sidebar width despite local storage value', () => {
mockSidebarWidth = 400;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
sidebarToggleState$={
new BehaviorSubject<SidebarToggleState>({
isCollapsed: true,
toggle: () => {},
})
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
initialState={{
sidebarWidth: 450,
}}
/>
);
expect(wrapper.find(ResizableLayout).prop('fixedPanelSize')).toBe(450);
});
it('should pass mode ResizableLayoutMode.Resizable when not mobile and sidebar is not collapsed', () => {
mockIsMobile = false;
const wrapper = mount(

View file

@ -13,17 +13,18 @@ import {
ResizableLayoutDirection,
ResizableLayoutMode,
} from '@kbn/resizable-layout';
import React, { useState, type ReactNode } from 'react';
import React, { useState, type ReactNode, useCallback, type ComponentProps } from 'react';
import { css } from '@emotion/react';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import useObservable from 'react-use/lib/useObservable';
import type { BehaviorSubject } from 'rxjs';
import type { SidebarToggleState } from '../../../types';
import { withRestorableState, useRestorableState } from './discover_layout_restorable_state';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export const SIDEBAR_WIDTH_KEY = 'discover:sidebarWidth';
export const DiscoverResizableLayout = ({
export const InternalDiscoverResizableLayout = ({
container,
sidebarToggleState$,
sidebarPanel,
@ -34,6 +35,7 @@ export const DiscoverResizableLayout = ({
sidebarPanel: ReactNode;
mainPanel: ReactNode;
}) => {
const { storage } = useDiscoverServices();
const [sidebarPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
@ -46,7 +48,24 @@ export const DiscoverResizableLayout = ({
const defaultSidebarWidth = euiTheme.base * 19;
const minMainPanelWidth = euiTheme.base * 24;
const [sidebarWidth, setSidebarWidth] = useLocalStorage(SIDEBAR_WIDTH_KEY, defaultSidebarWidth);
const [sidebarWidth, setSidebarWidth] = useRestorableState(
'sidebarWidth',
() => {
const widthInLocalStorage = Number(storage?.get(SIDEBAR_WIDTH_KEY));
return widthInLocalStorage || defaultSidebarWidth;
},
{
shouldStoreDefaultValueRightAway: true, // otherwise, it would re-initialize with the localStorage value which might get updated in the meantime
}
);
const setSidebarWidthAndUpdateInStorage = useCallback(
(width: number) => {
setSidebarWidth(width);
storage.set(SIDEBAR_WIDTH_KEY, width);
},
[setSidebarWidth, storage]
);
const sidebarToggleState = useObservable(sidebarToggleState$);
const isSidebarCollapsed = sidebarToggleState?.isCollapsed ?? false;
@ -74,12 +93,18 @@ export const DiscoverResizableLayout = ({
fixedPanel={<OutPortal node={sidebarPanelNode} />}
flexPanel={<OutPortal node={mainPanelNode} />}
data-test-subj="discoverLayout"
onFixedPanelSizeChange={setSidebarWidth}
onFixedPanelSizeChange={setSidebarWidthAndUpdateInStorage}
/>
</>
);
};
export const DiscoverResizableLayout = withRestorableState(
React.memo(InternalDiscoverResizableLayout)
);
export type DiscoverResizableLayoutProps = ComponentProps<typeof DiscoverResizableLayout>;
const dscPageBodyContentsCss = css`
overflow: hidden;
height: 100%;

View file

@ -187,7 +187,7 @@ function getStateContainer({
fieldListUiState,
}: {
query?: Query | AggregateQuery;
fieldListUiState?: UnifiedFieldListRestorableState;
fieldListUiState?: Partial<UnifiedFieldListRestorableState>;
}) {
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
stateContainer.appState.set({
@ -211,7 +211,7 @@ async function mountComponent<WithReactTestingLibrary extends boolean = false>(
props: TestWrapperProps,
appStateParams: {
query?: Query | AggregateQuery;
fieldListUiState?: UnifiedFieldListRestorableState;
fieldListUiState?: Partial<UnifiedFieldListRestorableState>;
} = {},
services?: DiscoverServices,
withReactTestingLibrary?: WithReactTestingLibrary
@ -581,6 +581,24 @@ describe('discover responsive sidebar', function () {
expect(findTestSubject(comp, 'fieldListFiltersFieldSearch').prop('value')).toBe('byte');
});
it('should restore collapsed state state after switching tabs', async function () {
const compCollapsed = await mountComponent(props, {
fieldListUiState: {
isCollapsed: true,
},
});
expect(findTestSubject(compCollapsed, 'fieldList').exists()).toBe(false);
const compExpanded = await mountComponent(props, {
fieldListUiState: {
isCollapsed: false,
},
});
expect(findTestSubject(compExpanded, 'fieldList').exists()).toBe(true);
});
it('should show "Add a field" button to create a runtime field', async () => {
const services = createMockServices();
const comp = await mountComponent(props, {}, services);

View file

@ -237,6 +237,14 @@ export const internalStateSlice = createSlice({
withTab(state, action, (tab) => {
tab.uiState.fieldList = action.payload.fieldListUiState;
}),
setLayoutUiState: (
state,
action: TabAction<{ layoutUiState: Partial<TabState['uiState']['layout']> }>
) =>
withTab(state, action, (tab) => {
tab.uiState.layout = action.payload.layoutUiState;
}),
},
extraReducers: (builder) => {
builder.addCase(loadDataViewList.fulfilled, (state, action) => {

View file

@ -16,6 +16,7 @@ import type { UnifiedFieldListRestorableState } from '@kbn/unified-field-list';
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram';
import type { TabItem } from '@kbn/unified-tabs';
import type { DiscoverAppState } from '../discover_app_state_container';
import type { DiscoverLayoutRestorableState } from '../../components/layout/discover_layout_restorable_state';
export enum LoadingStatus {
Uninitialized = 'uninitialized',
@ -78,6 +79,7 @@ export interface TabState extends TabItem {
uiState: {
dataGrid?: Partial<UnifiedDataTableRestorableState>;
fieldList?: Partial<UnifiedFieldListRestorableState>;
layout?: Partial<DiscoverLayoutRestorableState>;
};
}

View file

@ -12,4 +12,5 @@ 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;
export const TABS_ENABLED =
window.localStorage.getItem('discoverExperimental:tabs') === 'true' || false;

View file

@ -108,7 +108,8 @@
"@kbn/unified-histogram",
"@kbn/alerts-ui-shared",
"@kbn/core-pricing-browser-mocks",
"@kbn/css-utils"
"@kbn/css-utils",
"@kbn/restorable-state"
],
"exclude": [
"target/**/*"

View file

@ -0,0 +1,123 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const { discover, unifiedFieldList, unifiedTabs } = getPageObjects([
'common',
'discover',
'timePicker',
'header',
'unifiedFieldList',
'unifiedTabs',
]);
const retry = getService('retry');
const testSubjects = getService('testSubjects');
describe('tabs restorable state', function () {
describe('sidebar', function () {
it('should restore sidebar collapsible state', async function () {
const expectState = async (state: boolean) => {
expect(await discover.isSidebarPanelOpen()).to.be(state);
};
await expectState(true);
await unifiedTabs.createNewTab();
await discover.waitUntilTabIsLoaded();
await expectState(true);
await discover.closeSidebar();
await expectState(false);
await unifiedTabs.selectTab(0);
await discover.waitUntilTabIsLoaded();
await expectState(true);
await unifiedTabs.selectTab(1);
await discover.waitUntilTabIsLoaded();
await expectState(false);
await discover.openSidebar();
});
it('should restore sidebar width', async function () {
const distance = 100;
const initialWidth = await discover.getSidebarWidth();
const updatedWidth = initialWidth + distance;
const expectState = async (state: number) => {
expect(await discover.getSidebarWidth()).to.be(state);
};
await expectState(initialWidth);
await unifiedTabs.createNewTab();
await discover.waitUntilTabIsLoaded();
await expectState(initialWidth);
await discover.resizeSidebarBy(distance);
await expectState(updatedWidth);
await unifiedTabs.selectTab(0);
await discover.waitUntilTabIsLoaded();
await expectState(initialWidth);
await unifiedTabs.selectTab(1);
await discover.waitUntilTabIsLoaded();
await expectState(updatedWidth);
});
it('should restore sidebar filters', async function () {
const initialCount = 48;
const expectState = async (state: number) => {
expect(await unifiedFieldList.getSidebarSectionFieldCount('available')).to.be(state);
};
await expectState(initialCount);
await unifiedTabs.createNewTab();
await discover.waitUntilTabIsLoaded();
await expectState(initialCount);
await unifiedFieldList.findFieldByName('i');
await retry.try(async () => {
await expectState(28);
});
await unifiedTabs.createNewTab();
await discover.waitUntilTabIsLoaded();
await expectState(initialCount);
await unifiedFieldList.findFieldByName('e');
await retry.try(async () => {
await expectState(42);
});
await unifiedFieldList.openSidebarFieldFilter();
await testSubjects.click('typeFilter-number');
await unifiedFieldList.closeSidebarFieldFilter();
await retry.try(async () => {
await expectState(4);
});
await unifiedTabs.selectTab(0);
await discover.waitUntilTabIsLoaded();
await expectState(initialCount);
await unifiedTabs.selectTab(1);
await discover.waitUntilTabIsLoaded();
await expectState(28);
await unifiedTabs.selectTab(2);
await discover.waitUntilTabIsLoaded();
await expectState(4);
await unifiedFieldList.clearFieldSearchInput();
await unifiedFieldList.clearSidebarFieldFilters();
await retry.try(async () => {
await expectState(initialCount);
});
});
});
});
}

View file

@ -0,0 +1,19 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('.')],
};
}

View file

@ -0,0 +1,53 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const security = getService('security');
const { common, timePicker, discover } = getPageObjects(['common', 'timePicker', 'discover']);
describe('discover/tabs', function () {
before(async function () {
await browser.setWindowSize(1200, 800);
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
await kibanaServer.importExport.load(
'src/platform/test/functional/fixtures/kbn_archiver/discover'
);
await esArchiver.loadIfNeeded(
'src/platform/test/functional/fixtures/es_archiver/logstash_functional'
);
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
});
beforeEach(async () => {
await common.navigateToApp('home');
await browser.setLocalStorageItem('discoverExperimental:tabs', 'true');
await common.navigateToApp('discover');
await discover.waitUntilTabIsLoaded();
});
after(async () => {
await kibanaServer.importExport.unload(
'src/platform/test/functional/fixtures/kbn_archiver/discover'
);
await esArchiver.unload(
'src/platform/test/functional/fixtures/es_archiver/logstash_functional'
);
await kibanaServer.uiSettings.unset('defaultIndex');
await kibanaServer.savedObjects.cleanStandardList();
});
loadTestFile(require.resolve('./_restorable_state'));
});
}

View file

@ -105,6 +105,11 @@ export class DiscoverPageObject extends FtrService {
});
}
public async waitUntilTabIsLoaded() {
await this.header.waitUntilLoadingHasFinished();
await this.waitUntilSearchingHasFinished();
}
public async getColumnHeaders() {
return await this.dataGrid.getHeaderFields();
}
@ -440,6 +445,16 @@ export class DiscoverPageObject extends FtrService {
);
}
public async getSidebarWidth() {
const sidebar = await this.testSubjects.find('discover-sidebar');
return (await sidebar.getSize()).width;
}
public async resizeSidebarBy(distance: number) {
const resizeButton = await this.testSubjects.find('discoverLayoutResizableButton');
await this.browser.dragAndDrop({ location: resizeButton }, { location: { x: distance, y: 0 } });
}
public async editField(field: string) {
await this.retry.try(async () => {
await this.unifiedFieldList.pressEnterFieldListItemToggle(field);

View file

@ -95,6 +95,14 @@ export class UnifiedFieldListPageObject extends FtrService {
);
}
public async getSidebarSectionFieldCount(sectionName: SidebarSectionName): Promise<number> {
const counter = await this.find.byCssSelector(
`[data-test-subj="${this.getSidebarSectionSelector(sectionName)}-count"]`
);
return Number(await counter.getVisibleText());
}
public async toggleSidebarSection(sectionName: SidebarSectionName) {
return await this.find.clickByCssSelector(
`${this.getSidebarSectionSelector(sectionName, true)} .euiAccordion__arrow`
@ -277,6 +285,12 @@ export class UnifiedFieldListPageObject extends FtrService {
});
}
public async clearSidebarFieldFilters() {
await this.openSidebarFieldFilter();
await this.testSubjects.click('fieldListFiltersFieldTypeFilterClearAll');
await this.closeSidebarFieldFilter();
}
public async getFieldStatsViewType(): Promise<
| 'topValuesAndDistribution'
| 'histogram'