[UnifiedFieldList] Persist field list sections state in local storage (#148373)

Part of https://github.com/elastic/kibana/issues/137779

## Summary

This PR uses localStorage to persist which list sections user prefers to
expand/collapse per app (discover and lens state is saved separately).

<img width="911" alt="Screenshot 2023-01-04 at 12 26 16"
src="https://user-images.githubusercontent.com/1415710/210545295-65581197-d6e4-407f-a034-7a75de7feb3a.png">



### Checklist

- [ ] [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
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Julia Rechkunova 2023-01-05 13:09:42 +01:00 committed by GitHub
parent bec8b8c890
commit 19fd0c19fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 87 additions and 3 deletions

View file

@ -377,6 +377,7 @@ export function DiscoverSidebarComponent({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
screenReaderDescriptionId={fieldSearchDescriptionId}
localStorageKeyPrefix="discover"
/>
)}
</EuiFlexItem>

View file

@ -431,4 +431,54 @@ describe('UnifiedFieldList <FieldListGrouped /> + 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]);
});
});

View file

@ -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<string, boolean>;
function getDisplayedFieldsLength<T extends FieldListItem>(
fieldGroups: FieldListGroups<T>,
accordionState: Partial<Record<string, boolean>>
accordionState: InitiallyOpenSections
) {
return Object.entries(fieldGroups)
.filter(([key]) => accordionState[key])
@ -35,6 +39,7 @@ export interface FieldListGroupedProps<T extends FieldListItem> {
renderFieldItem: FieldsAccordionProps<T>['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<T extends FieldListItem = DataViewField>({
renderFieldItem,
scrollToTopResetCounter,
screenReaderDescriptionId,
localStorageKeyPrefix,
'data-test-subj': dataTestSubject = 'fieldListGrouped',
}: FieldListGroupedProps<T>) {
const hasSyncedExistingFields =
@ -56,9 +62,22 @@ function InnerFieldListGrouped<T extends FieldListItem = DataViewField>({
);
const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
const [accordionState, setAccordionState] = useState<Partial<Record<string, boolean>>>(() =>
const [storedInitiallyOpenSections, storeInitiallyOpenSections] =
useLocalStorage<InitiallyOpenSections>(
`${localStorageKeyPrefix ? localStorageKeyPrefix + '.' : ''}${LOCAL_STORAGE_KEY_SECTIONS}`,
{}
);
const [accordionState, setAccordionState] = useState<InitiallyOpenSections>(() =>
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<T extends FieldListItem = DataViewField>({
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?

View file

@ -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 () {

View file

@ -463,6 +463,10 @@ export class DiscoverPageObject extends FtrService {
).getAttribute('innerText');
}
public async cleanSidebarLocalStorage(): Promise<void> {
await this.browser.setLocalStorageItem('discover.unifiedFieldList.initiallyOpenSections', '{}');
}
public async waitUntilSidebarHasLoaded() {
await this.retry.waitFor('sidebar is loaded', async () => {
return (await this.getSidebarAriaDescription()).length > 0;

View file

@ -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 () => {

View file

@ -428,6 +428,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsIndexPattern"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>

View file

@ -161,6 +161,7 @@ export function TextBasedDataPanel({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsTextBasedLanguages"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>