diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 51cdaadd1cda2..705299c1566d2 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -377,6 +377,7 @@ export function DiscoverSidebarComponent({ {...fieldListGroupedProps} renderFieldItem={renderFieldItem} screenReaderDescriptionId={fieldSearchDescriptionId} + localStorageKeyPrefix="discover" /> )} diff --git a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx index 778f38168e6c1..9190c6de2859e 100644 --- a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx @@ -431,4 +431,54 @@ describe('UnifiedFieldList + useGroupedFields()', () => { '2 selected fields. 10 popular fields. 25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' ); }); + + it('persists sections state in local storage', async () => { + const wrapper = await mountGroupedList({ + listProps: { + ...defaultProps, + fieldsExistenceStatus: ExistenceFetchStatus.succeeded, + localStorageKeyPrefix: 'test', + }, + hookParams: { + dataViewId: dataView.id!, + allFields: manyFields, + }, + }); + + // only Available is open + expect( + wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('initialIsOpen')) + ).toStrictEqual([true, false, false, false]); + + await act(async () => { + await wrapper + .find('[data-test-subj="fieldListGroupedEmptyFields"]') + .find('button') + .first() + .simulate('click'); + await wrapper.update(); + }); + + // now Empty is open too + expect( + wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('initialIsOpen')) + ).toStrictEqual([true, false, true, false]); + + const wrapper2 = await mountGroupedList({ + listProps: { + ...defaultProps, + fieldsExistenceStatus: ExistenceFetchStatus.succeeded, + localStorageKeyPrefix: 'test', + }, + hookParams: { + dataViewId: dataView.id!, + allFields: manyFields, + }, + }); + + // both Available and Empty are open for the second instance + expect( + wrapper2.find(FieldsAccordion).map((accordion) => accordion.prop('initialIsOpen')) + ).toStrictEqual([true, false, true, false]); + }); }); diff --git a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx index 9e81cb8c5d476..1bc84a37ed7e0 100644 --- a/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx @@ -8,6 +8,7 @@ import { partition, throttle } from 'lodash'; import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { i18n } from '@kbn/i18n'; import { EuiScreenReaderOnly, EuiSpacer } from '@elastic/eui'; import { type DataViewField } from '@kbn/data-views-plugin/common'; @@ -18,10 +19,13 @@ import { ExistenceFetchStatus, FieldsGroup, FieldsGroupNames } from '../../types import './field_list_grouped.scss'; const PAGINATION_SIZE = 50; +export const LOCAL_STORAGE_KEY_SECTIONS = 'unifiedFieldList.initiallyOpenSections'; + +type InitiallyOpenSections = Record; function getDisplayedFieldsLength( fieldGroups: FieldListGroups, - accordionState: Partial> + accordionState: InitiallyOpenSections ) { return Object.entries(fieldGroups) .filter(([key]) => accordionState[key]) @@ -35,6 +39,7 @@ export interface FieldListGroupedProps { renderFieldItem: FieldsAccordionProps['renderFieldItem']; scrollToTopResetCounter: number; screenReaderDescriptionId?: string; + localStorageKeyPrefix?: string; // Your app name: "discover", "lens", etc. If not provided, sections state would not be persisted. 'data-test-subj'?: string; } @@ -45,6 +50,7 @@ function InnerFieldListGrouped({ renderFieldItem, scrollToTopResetCounter, screenReaderDescriptionId, + localStorageKeyPrefix, 'data-test-subj': dataTestSubject = 'fieldListGrouped', }: FieldListGroupedProps) { const hasSyncedExistingFields = @@ -56,9 +62,22 @@ function InnerFieldListGrouped({ ); const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); - const [accordionState, setAccordionState] = useState>>(() => + const [storedInitiallyOpenSections, storeInitiallyOpenSections] = + useLocalStorage( + `${localStorageKeyPrefix ? localStorageKeyPrefix + '.' : ''}${LOCAL_STORAGE_KEY_SECTIONS}`, + {} + ); + const [accordionState, setAccordionState] = useState(() => Object.fromEntries( - fieldGroupsToShow.map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) + fieldGroupsToShow.map(([key, { isInitiallyOpen }]) => { + const storedInitiallyOpen = localStorageKeyPrefix + ? storedInitiallyOpenSections?.[key] + : null; // from localStorage + return [ + key, + typeof storedInitiallyOpen === 'boolean' ? storedInitiallyOpen : isInitiallyOpen, + ]; + }) ) ); @@ -256,6 +275,12 @@ function InnerFieldListGrouped({ Math.min(Math.ceil(pageSize * 1.5), displayedFieldLength) ) ); + if (localStorageKeyPrefix) { + storeInitiallyOpenSections({ + ...storedInitiallyOpenSections, + [key]: open, + }); + } }} showExistenceFetchError={fieldsExistenceStatus === ExistenceFetchStatus.failed} showExistenceFetchTimeout={fieldsExistenceStatus === ExistenceFetchStatus.failed} // TODO: deprecate timeout logic? diff --git a/test/functional/apps/discover/group1/_sidebar.ts b/test/functional/apps/discover/group1/_sidebar.ts index 66c60d22b4d3f..a62a379c20224 100644 --- a/test/functional/apps/discover/group1/_sidebar.ts +++ b/test/functional/apps/discover/group1/_sidebar.ts @@ -45,6 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.uiSettings.replace({}); + await PageObjects.discover.cleanSidebarLocalStorage(); }); describe('field filtering', function () { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index b9be3c673448d..7db95f8063c12 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -463,6 +463,10 @@ export class DiscoverPageObject extends FtrService { ).getAttribute('innerText'); } + public async cleanSidebarLocalStorage(): Promise { + await this.browser.setLocalStorageItem('discover.unifiedFieldList.initiallyOpenSections', '{}'); + } + public async waitUntilSidebarHasLoaded() { await this.retry.waitFor('sidebar is loaded', async () => { return (await this.getSidebarAriaDescription()).length > 0; diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx index 411b8cd094ab2..f774525518e49 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx @@ -374,6 +374,7 @@ describe('FormBased Data Panel', () => { (UseExistingFieldsApi.useExistingFieldsReader as jest.Mock).mockClear(); (UseExistingFieldsApi.useExistingFieldsFetcher as jest.Mock).mockClear(); UseExistingFieldsApi.resetExistingFieldsCache(); + window.localStorage.removeItem('lens.unifiedFieldList.initiallyOpenSections'); }); it('should render a warning if there are no index patterns', async () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index 01feaa4187627..374eb430dae9c 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -428,6 +428,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ {...fieldListGroupedProps} renderFieldItem={renderFieldItem} data-test-subj="lnsIndexPattern" + localStorageKeyPrefix="lens" /> diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx index b278284bad8e9..aad9bae11faf4 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx @@ -161,6 +161,7 @@ export function TextBasedDataPanel({ {...fieldListGroupedProps} renderFieldItem={renderFieldItem} data-test-subj="lnsTextBasedLanguages" + localStorageKeyPrefix="lens" />