[Discover] Improve sidebar rendering for large field lists (#95038) (#95764)

* 50 fields are rendered initially, more fields are rendered while user is scrolling

* This is a big performance improvement when there are lots of fields to render
This commit is contained in:
Matthias Wilhelm 2021-03-30 14:41:47 +02:00 committed by GitHub
parent 8961505b55
commit 630f00897e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 24 deletions

View file

@ -7,7 +7,8 @@
*/
import './discover_sidebar.scss';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { throttle } from 'lodash';
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiAccordion,
@ -18,7 +19,9 @@ import {
EuiSpacer,
EuiNotificationBadge,
EuiPageSideBar,
useResizeObserver,
} from '@elastic/eui';
import { isEqual, sortBy } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { DiscoverField } from './discover_field';
@ -32,6 +35,11 @@ import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './l
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
/**
* Default number of available fields displayed and added on scroll
*/
const FIELDS_PER_PAGE = 50;
export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
/**
* Current state of the field filter, filtering fields by name, type, ...
@ -66,18 +74,25 @@ export function DiscoverSidebar({
unmappedFieldsConfig,
}: DiscoverSidebarProps) {
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
const [scrollContainer, setScrollContainer] = useState<Element | null>(null);
const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE);
const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE);
const availableFieldsContainer = useRef<HTMLUListElement | null>(null);
useEffect(() => {
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
setFields(newFields);
}, [selectedIndexPattern, fieldCounts, hits]);
const scrollDimensions = useResizeObserver(scrollContainer);
const onChangeFieldSearch = useCallback(
(field: string, value: string | boolean | undefined) => {
const newState = setFieldFilterProp(fieldFilter, field, value);
setFieldFilter(newState);
setFieldsToRender(fieldsPerPage);
},
[fieldFilter, setFieldFilter]
[fieldFilter, setFieldFilter, setFieldsToRender, fieldsPerPage]
);
const getDetailsByField = useCallback(
@ -85,7 +100,10 @@ export function DiscoverSidebar({
[hits, columns, selectedIndexPattern]
);
const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING);
const popularLimit = useMemo(() => services.uiSettings.get(FIELDS_LIMIT_SETTING), [
services.uiSettings,
]);
const {
selected: selectedFields,
popular: popularFields,
@ -112,6 +130,50 @@ export function DiscoverSidebar({
]
);
const paginate = useCallback(() => {
const newFieldsToRender = fieldsToRender + Math.round(fieldsPerPage * 0.5);
setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, unpopularFields.length)));
}, [setFieldsToRender, fieldsToRender, unpopularFields, fieldsPerPage]);
useEffect(() => {
if (scrollContainer && unpopularFields.length && availableFieldsContainer.current) {
const { clientHeight, scrollHeight } = scrollContainer;
const isScrollable = scrollHeight > clientHeight; // there is no scrolling currently
const allFieldsRendered = fieldsToRender >= unpopularFields.length;
if (!isScrollable && !allFieldsRendered) {
// Not all available fields were rendered with the given fieldsPerPage number
// and no scrolling is available due to the a high zoom out factor of the browser
// In this case the fieldsPerPage needs to be adapted
const fieldsRenderedHeight = availableFieldsContainer.current.clientHeight;
const avgHeightPerItem = Math.round(fieldsRenderedHeight / fieldsToRender);
const newFieldsPerPage = Math.round(clientHeight / avgHeightPerItem) + 10;
if (newFieldsPerPage >= FIELDS_PER_PAGE && newFieldsPerPage !== fieldsPerPage) {
setFieldsPerPage(newFieldsPerPage);
setFieldsToRender(newFieldsPerPage);
}
}
}
}, [
fieldsPerPage,
scrollContainer,
unpopularFields,
fieldsToRender,
setFieldsPerPage,
setFieldsToRender,
scrollDimensions,
]);
const lazyScroll = useCallback(() => {
if (scrollContainer) {
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
const nearBottom = scrollTop + clientHeight > scrollHeight * 0.9;
if (nearBottom && unpopularFields) {
paginate();
}
}
}, [paginate, scrollContainer, unpopularFields]);
const fieldTypes = useMemo(() => {
const result = ['any'];
if (Array.isArray(fields)) {
@ -145,12 +207,19 @@ export function DiscoverSidebar({
return map;
}, [fields, useNewFieldsApi, selectedFields]);
const getPaginated = useCallback(
(list) => {
return list.slice(0, fieldsToRender);
},
[fieldsToRender]
);
const filterChanged = useMemo(() => isEqual(fieldFilter, getDefaultFieldFilter()), [fieldFilter]);
if (!selectedIndexPattern || !fields) {
return null;
}
const filterChanged = isEqual(fieldFilter, getDefaultFieldFilter());
if (useFlyout) {
return (
<section
@ -207,9 +276,18 @@ export function DiscoverSidebar({
</form>
</EuiFlexItem>
<EuiFlexItem className="eui-yScroll">
<div>
<div
ref={(el) => {
if (el && !el.dataset.dynamicScroll) {
el.dataset.dynamicScroll = 'true';
setScrollContainer(el);
}
}}
onScroll={throttle(lazyScroll, 100)}
className="eui-yScroll"
>
{fields.length > 0 && (
<>
<div>
{selectedFields &&
selectedFields.length > 0 &&
selectedFields[0].displayName !== '_source' ? (
@ -241,11 +319,7 @@ export function DiscoverSidebar({
>
{selectedFields.map((field: IndexPatternField) => {
return (
<li
key={`field${field.name}`}
data-attr-field={field.name}
className="dscSidebar__item"
>
<li key={`field${field.name}`} data-attr-field={field.name}>
<DiscoverField
alwaysShowActionButton={alwaysShowActionButtons}
field={field}
@ -303,11 +377,7 @@ export function DiscoverSidebar({
>
{popularFields.map((field: IndexPatternField) => {
return (
<li
key={`field${field.name}`}
data-attr-field={field.name}
className="dscSidebar__item"
>
<li key={`field${field.name}`} data-attr-field={field.name}>
<DiscoverField
alwaysShowActionButton={alwaysShowActionButtons}
field={field}
@ -329,14 +399,11 @@ export function DiscoverSidebar({
className="dscFieldList dscFieldList--unpopular"
aria-labelledby="available_fields"
data-test-subj={`fieldList-unpopular`}
ref={availableFieldsContainer}
>
{unpopularFields.map((field: IndexPatternField) => {
{getPaginated(unpopularFields).map((field: IndexPatternField) => {
return (
<li
key={`field${field.name}`}
data-attr-field={field.name}
className="dscSidebar__item"
>
<li key={`field${field.name}`} data-attr-field={field.name}>
<DiscoverField
alwaysShowActionButton={alwaysShowActionButtons}
field={field}
@ -353,7 +420,7 @@ export function DiscoverSidebar({
})}
</ul>
</EuiAccordion>
</>
</div>
)}
</div>
</EuiFlexItem>

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']);
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
describe('test large number of fields in sidebar', function () {
before(async function () {
await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false);
await esArchiver.loadIfNeeded('large_fields');
await PageObjects.settings.navigateTo();
await kibanaServer.uiSettings.update({
'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`,
});
await PageObjects.settings.createIndexPattern('*huge*', 'date', true);
await PageObjects.common.navigateToApp('discover');
});
it('test_huge data should have expected number of fields', async function () {
await PageObjects.discover.selectIndexPattern('*huge*');
// initially this field should not be rendered
const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050');
expect(fieldExistsBeforeScrolling).to.be(false);
// scrolling down a little, should render this field
await testSubjects.scrollIntoView('fieldToggle-myvar1029');
const fieldExistsAfterScrolling = await testSubjects.exists('field-myvar1050');
expect(fieldExistsAfterScrolling).to.be(true);
});
after(async () => {
await security.testUser.restoreDefaults();
await esArchiver.unload('large_fields');
await kibanaServer.uiSettings.replace({});
});
});
}

View file

@ -47,5 +47,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_grid_doc_navigation'));
loadTestFile(require.resolve('./_data_grid_doc_table'));
loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields'));
loadTestFile(require.resolve('./_huge_fields'));
});
}