mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
bec8b8c890
commit
19fd0c19fa
8 changed files with 87 additions and 3 deletions
|
@ -377,6 +377,7 @@ export function DiscoverSidebarComponent({
|
|||
{...fieldListGroupedProps}
|
||||
renderFieldItem={renderFieldItem}
|
||||
screenReaderDescriptionId={fieldSearchDescriptionId}
|
||||
localStorageKeyPrefix="discover"
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -428,6 +428,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
|
|||
{...fieldListGroupedProps}
|
||||
renderFieldItem={renderFieldItem}
|
||||
data-test-subj="lnsIndexPattern"
|
||||
localStorageKeyPrefix="lens"
|
||||
/>
|
||||
</FieldList>
|
||||
</ChildDragDropProvider>
|
||||
|
|
|
@ -161,6 +161,7 @@ export function TextBasedDataPanel({
|
|||
{...fieldListGroupedProps}
|
||||
renderFieldItem={renderFieldItem}
|
||||
data-test-subj="lnsTextBasedLanguages"
|
||||
localStorageKeyPrefix="lens"
|
||||
/>
|
||||
</FieldList>
|
||||
</ChildDragDropProvider>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue