mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
170670cfa1
commit
d0da9f94c6
22 changed files with 451 additions and 30 deletions
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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()) ||
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>();
|
|
@ -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(
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
19
src/platform/test/functional/apps/discover/tabs/config.ts
Normal file
19
src/platform/test/functional/apps/discover/tabs/config.ts
Normal 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('.')],
|
||||
};
|
||||
}
|
53
src/platform/test/functional/apps/discover/tabs/index.ts
Normal file
53
src/platform/test/functional/apps/discover/tabs/index.ts
Normal 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'));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue