From 68f88058d476b380e1216c5e976be50d5c515f33 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 4 Jan 2023 12:23:13 +0100 Subject: [PATCH] [UnifiedFieldList] Persist field list sections state in local storage --- .../components/sidebar/discover_sidebar.tsx | 1 + .../field_list_grouped.test.tsx | 50 +++++++++++++++++++ .../field_list_grouped/field_list_grouped.tsx | 31 ++++++++++-- .../datasources/form_based/datapanel.tsx | 1 + .../datasources/text_based/datapanel.tsx | 1 + 5 files changed, 81 insertions(+), 3 deletions(-) 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 75bfdffa79627..6184e7751c679 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/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" />