Skip to content

Commit

Permalink
[UnifiedFieldList] Persist field list sections state in local storage (
Browse files Browse the repository at this point in the history
…#148373)

Part of #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 <[email protected]>
  • Loading branch information
jughosta and stratoula authored Jan 5, 2023
1 parent bec8b8c commit 19fd0c1
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export function DiscoverSidebarComponent({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
screenReaderDescriptionId={fieldSearchDescriptionId}
localStorageKeyPrefix="discover"
/>
)}
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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])
Expand All @@ -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;
}

Expand All @@ -45,6 +50,7 @@ function InnerFieldListGrouped<T extends FieldListItem = DataViewField>({
renderFieldItem,
scrollToTopResetCounter,
screenReaderDescriptionId,
localStorageKeyPrefix,
'data-test-subj': dataTestSubject = 'fieldListGrouped',
}: FieldListGroupedProps<T>) {
const hasSyncedExistingFields =
Expand All @@ -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,
];
})
)
);

Expand Down Expand Up @@ -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?
Expand Down
1 change: 1 addition & 0 deletions test/functional/apps/discover/group1/_sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
4 changes: 4 additions & 0 deletions test/functional/page_objects/discover_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsIndexPattern"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export function TextBasedDataPanel({
{...fieldListGroupedProps}
renderFieldItem={renderFieldItem}
data-test-subj="lnsTextBasedLanguages"
localStorageKeyPrefix="lens"
/>
</FieldList>
</ChildDragDropProvider>
Expand Down

0 comments on commit 19fd0c1

Please sign in to comment.