Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard] [Controls] Load more options list suggestions on scroll #148331

Merged
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
861b994
Add non-functional UI components
Heenawter Dec 30, 2022
0c74596
Add cardinality
Heenawter Dec 30, 2022
22580cd
Add rough pagination
Heenawter Dec 30, 2022
1609cdb
Add page to component state
Heenawter Dec 30, 2022
5511e87
Grab 100 results on page 2 and paginate client-side
Heenawter Jan 3, 2023
4d8bdd6
Update pagination strategy
Heenawter Jan 3, 2023
b78427b
Fix broken suggestions after adding document count
Heenawter Jan 11, 2023
33587c5
Remove double linking of keyword/keyword+text fields
Heenawter Jan 11, 2023
83db01d
Fix cardinality
Heenawter Jan 12, 2023
f1911b5
Reset page on sort change
Heenawter Jan 13, 2023
d140942
Load more on scroll rather than through pagination
Heenawter Jan 17, 2023
6d26bf7
First attempt at loading mask
Heenawter Jan 18, 2023
f68a3dd
Make loading state nicer
Heenawter Jan 19, 2023
9b04b01
Add "Reached the end" text
Heenawter Jan 19, 2023
c11f53e
Add tooltip to cardinality badge + replace placeholder text
Heenawter Jan 19, 2023
bec4895
Remove size from component state, simplify logic, and clean up code
Heenawter Jan 19, 2023
cbe46f4
Test nested queries
Heenawter Jan 23, 2023
23f9b1a
Fix double request by not using debounced load
Heenawter Jan 23, 2023
ec90a21
Fix nested queries
Heenawter Jan 23, 2023
0a23660
Create cheap vs expensive suggestion aggregation builders
Heenawter Jan 24, 2023
0122f83
Create memoized service for allowExpensive check
Heenawter Jan 24, 2023
28a7dbc
Add warning when allowExpensive is off
Heenawter Jan 24, 2023
a8a9814
Fix memory leak + better error handling
Heenawter Jan 24, 2023
4850500
Close popover on unmount + fatal error
Heenawter Jan 25, 2023
d67ed5f
Fix style of error state
Heenawter Jan 25, 2023
2905f17
Clean up type guard + styling
Heenawter Jan 25, 2023
3680011
Treat undefined the same as `true`
kibanamachine Jan 25, 2023
3a0ad82
Add unit tests + functional tests
Heenawter Jan 26, 2023
e87194a
Fix expensive queries + add unit tests
Heenawter Jan 30, 2023
8bf1d0f
Add max options list constant
Heenawter Jan 30, 2023
cb4c905
Fix logic of expensive queries check + cheap queries
Heenawter Jan 30, 2023
9345d00
Make `allowExpensiveQueries` a part of component state instead
Heenawter Jan 30, 2023
fe0a21d
Clean up and fix flakiness of new tests
Heenawter Jan 30, 2023
cec4c06
Fix accessibility of badges
Heenawter Jan 31, 2023
31b2927
Update `allow_expensive_queries` warning tooltip
Heenawter Jan 31, 2023
0a12e43
Undo changes to `jest_setup` and mocks
Heenawter Jan 31, 2023
6222e32
Merge branch 'main' into add-pagination-to-options-list_2022-12-30
kibanamachine Feb 1, 2023
6b7ab49
Move cardinality + actions to below search bar
Heenawter Feb 2, 2023
9d258da
Merge branch 'add-pagination-to-options-list_2022-12-30' of github.co…
Heenawter Feb 2, 2023
eaa32bc
Clean up linting errors
Heenawter Feb 2, 2023
b87c5bd
Change location of sorting
Heenawter Feb 2, 2023
3485cb9
Disable sort button and add tooltip when show only selected
Heenawter Feb 6, 2023
01b35d5
Hide cardinality when allow expensive queries false
Heenawter Feb 6, 2023
6e7d205
Address all feedback except cluster settings
Heenawter Feb 6, 2023
88194a1
Merge branch 'main' into add-pagination-to-options-list_2022-12-30
Heenawter Feb 6, 2023
c366387
Fix typo
Heenawter Feb 6, 2023
380de4d
Fix logic of cluster settings route
Heenawter Feb 6, 2023
5801d9d
Fix grabbing of cardinality text
Heenawter Feb 6, 2023
7f3f5f7
Remove `.only`
Heenawter Feb 6, 2023
a78459d
Change message for control frame fatal error
Heenawter Feb 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/plugins/controls/common/options_list/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { createReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public/redux_embeddables/create_redux_embeddable_tools';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';

import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public';
import { OptionsListComponentState, OptionsListReduxState } from '../../public/options_list/types';
Expand All @@ -17,6 +18,29 @@ import {
import { ControlFactory, ControlOutput } from '../../public/types';
import { OptionsListEmbeddableInput } from './types';

export const mockOptionsListDataView = {
spec: {
id: 'sample id',
fields: {
'sample field': {
name: 'sample field',
type: 'string',
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
count: 20,
readFromDocValues: true,
scripted: false,
isMapped: true,
},
},
runtimeFieldMap: {},
},
fieldFormats: fieldFormatsMock,
shortDotsEnable: false,
metaFields: [],
};

const mockOptionsListComponentState = {
...getDefaultComponentState(),
field: undefined,
Expand Down
36 changes: 21 additions & 15 deletions src/plugins/controls/common/options_list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import { FieldSpec, DataView, RuntimeFieldSpec } from '@kbn/data-views-plugin/common';
import type { Filter, Query, BoolQuery, TimeRange } from '@kbn/es-query';

import { OptionsListSortingType } from './suggestions_sorting';
import { DataControlInput } from '../types';
import type { OptionsListSortingType } from './suggestions_sorting';
import type { DataControlInput } from '../types';

export const OPTIONS_LIST_CONTROL = 'optionsListControl';

Expand All @@ -20,46 +20,52 @@ export interface OptionsListEmbeddableInput extends DataControlInput {
existsSelected?: boolean;
runPastTimeout?: boolean;
singleSelect?: boolean;
hideActionBar?: boolean;
hideExclude?: boolean;
hideExists?: boolean;
hideFooter?: boolean;
Copy link
Contributor Author

@Heenawter Heenawter Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we were adding the cardinality badge beside the search bar, the action bar was starting to get pretty messy (especially when you also had ignored selections). So, we decided to move the "Clear all selections" and "Show only selected" buttons to the footer, like so:

Screenshot 2023-01-30 at 11 34 47 AM

Previously, the hideExcludes property would hide the entire footer - however, now that some other buttons have been moved to this footer, that no longer makes sense. Since I know that consumers of the ControlGroupRenderer want control over which parts of the popover are shown, I went ahead and added the hideFooter property, which should be used instead of the hideExcludes property if you want the entire footer to be hidden:

image

The hideExcludes property now literally just hides the include/exclude button group, like so:

image

Note that there is no UI to set these properties - these are intended specifically for consumers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@Heenawter Heenawter Feb 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: We have moved to the following design to display the cardinality rather than a badge:

Screenshot 2023-02-02 at 4 48 57 PM

So, I've modified the code to make hideExclude once again hide the entire footer, which makes hideFooter no longer necessary. Resolved in 6b7ab49

hideSort?: boolean;
hideActionBar?: boolean;
exclude?: boolean;
placeholder?: string;
}

export type OptionsListField = FieldSpec & {
Heenawter marked this conversation as resolved.
Show resolved Hide resolved
textFieldName?: string;
parentFieldName?: string;
childFieldName?: string;
};

export interface OptionsListSuggestions {
[key: string]: { doc_count: number };
}

export interface OptionsListSuggestionResult {
ThomThomson marked this conversation as resolved.
Show resolved Hide resolved
suggestions: OptionsListSuggestions;
totalCardinality?: number; // total cardinality will be undefined when `useExpensiveQueries` is `false`
}

/**
* The Options list response is returned from the serverside Options List route.
*/
export interface OptionsListResponse {
rejected: boolean;
export interface OptionsListSuccessResponse {
suggestions: OptionsListSuggestions;
totalCardinality: number;
totalCardinality?: number;
invalidSelections?: string[];
}

export interface OptionsListFailureResponse {
error: 'aborted' | Error;
}

export type OptionsListResponse = OptionsListSuccessResponse | OptionsListFailureResponse;
Heenawter marked this conversation as resolved.
Show resolved Hide resolved

/**
* The Options list request type taken in by the public Options List service.
*/
export type OptionsListRequest = Omit<
OptionsListRequestBody,
'filters' | 'fieldName' | 'fieldSpec' | 'textFieldName'
> & {
allowExpensiveQueries: boolean;
timeRange?: TimeRange;
field: OptionsListField;
runPastTimeout?: boolean;
dataView: DataView;
filters?: Filter[];
field: FieldSpec;
query?: Query;
};

Expand All @@ -68,13 +74,13 @@ export type OptionsListRequest = Omit<
*/
export interface OptionsListRequestBody {
runtimeFieldMap?: Record<string, RuntimeFieldSpec>;
allowExpensiveQueries: boolean;
sort?: OptionsListSortingType;
filters?: Array<{ bool: BoolQuery }>;
selectedOptions?: string[];
runPastTimeout?: boolean;
parentFieldName?: string;
textFieldName?: string;
searchString?: string;
fieldSpec?: FieldSpec;
fieldName: string;
size: number;
}
2 changes: 0 additions & 2 deletions src/plugins/controls/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,5 @@ export type ControlInput = EmbeddableInput & {

export type DataControlInput = ControlInput & {
fieldName: string;
parentFieldName?: string;
childFieldName?: string;
dataViewId: string;
};
2 changes: 1 addition & 1 deletion src/plugins/controls/jest_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

// Start the services with stubs
import { pluginServices } from './public/services';
import { registry } from './public/services/plugin_services.story';
import { registry } from './public/services/plugin_services.stub';

registry.start({});
pluginServices.setRegistry(registry);
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ const storybookStubOptionsListRequest = async (
{}
),
totalCardinality: 100,
rejected: false,
}),
120
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@
import React, { useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFormControlLayout,
EuiFormLabel,
EuiFormRow,
EuiIcon,
EuiLink,
EuiLoadingChart,
EuiPopover,
EuiText,
EuiToolTip,
} from '@elastic/eui';

Expand All @@ -40,25 +38,26 @@ interface ControlFrameErrorProps {
const ControlFrameError = ({ error }: ControlFrameErrorProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const popoverButton = (
<EuiText className="errorEmbeddableCompact__button" size="xs">
<EuiLink
className="eui-textTruncate"
color="subdued"
onClick={() => setPopoverOpen((open) => !open)}
>
<EuiIcon type="alert" color="danger" />
<FormattedMessage
id="controls.frame.error.message"
defaultMessage="An error has occurred. Read more"
/>
</EuiLink>
</EuiText>
<EuiButtonEmpty
color="danger"
iconSize="m"
iconType={'alert'}
onClick={() => setPopoverOpen((open) => !open)}
className={'errorEmbeddableCompact__button'}
textProps={{ className: 'errorEmbeddableCompact__text' }}
>
<FormattedMessage
id="controls.frame.error.message"
defaultMessage="An error has occurred. Read more"
/>
</EuiButtonEmpty>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorStateBeforeAfter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to say something more specific about the error in the message? Is "Read more" a link and if so, what does it link to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire area is actually a button, and clicking on it will open a popover with a very brief description of the error.

I could change this text to be something like "An error has occurred. Click here to read more." to be more specific? 👀

);

return (
<EuiPopover
button={popoverButton}
isOpen={isPopoverOpen}
className="errorEmbeddableCompact__popover"
anchorClassName="errorEmbeddableCompact__popoverAnchor"
closePopover={() => setPopoverOpen(false)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,8 @@ export const ControlEditor = ({
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
const { parentFieldName, childFieldName } = fieldRegistry?.[field.name] ?? {};
onTypeEditorChange({
fieldName: field.name,
...(parentFieldName && { parentFieldName }),
...(childFieldName && { childFieldName }),
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@

import { memoize } from 'lodash';

import { IFieldSubTypeMulti } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/common';

import { pluginServices } from '../../services';
import { DataControlFieldRegistry, IEditableControlFactory } from '../../types';
import { DataControlField, DataControlFieldRegistry, IEditableControlFactory } from '../../types';

export const getDataControlFieldRegistry = memoize(
async (dataView: DataView) => {
Expand All @@ -21,50 +20,30 @@ export const getDataControlFieldRegistry = memoize(
(dataView: DataView) => [dataView.id, JSON.stringify(dataView.fields.getAll())].join('|')
);

const doubleLinkFields = (dataView: DataView) => {
// double link the parent-child relationship specifically for case-sensitivity support for options lists
const fieldRegistry: DataControlFieldRegistry = {};

for (const field of dataView.fields.getAll()) {
if (!fieldRegistry[field.name]) {
fieldRegistry[field.name] = { field, compatibleControlTypes: [] };
}

const parentFieldName = (field.subType as IFieldSubTypeMulti)?.multi?.parent;
if (parentFieldName) {
fieldRegistry[field.name].parentFieldName = parentFieldName;

const parentField = dataView.getFieldByName(parentFieldName);
if (!fieldRegistry[parentFieldName] && parentField) {
fieldRegistry[parentFieldName] = { field: parentField, compatibleControlTypes: [] };
}
fieldRegistry[parentFieldName].childFieldName = field.name;
}
}
return fieldRegistry;
};

const loadFieldRegistryFromDataView = async (
dataView: DataView
): Promise<DataControlFieldRegistry> => {
const {
controls: { getControlTypes, getControlFactory },
} = pluginServices.getServices();
const newFieldRegistry: DataControlFieldRegistry = doubleLinkFields(dataView);

const controlFactories = getControlTypes().map(
(controlType) => getControlFactory(controlType) as IEditableControlFactory
);
dataView.fields.map((dataViewField) => {
for (const factory of controlFactories) {
if (factory.isFieldCompatible) {
factory.isFieldCompatible(newFieldRegistry[dataViewField.name]);
const fieldRegistry: DataControlFieldRegistry = dataView.fields
.getAll()
.reduce((registry, field) => {
const test: DataControlField = { field, compatibleControlTypes: [] };
for (const factory of controlFactories) {
if (factory.isFieldCompatible) {
factory.isFieldCompatible(test);
}
}
}

if (newFieldRegistry[dataViewField.name]?.compatibleControlTypes.length === 0) {
delete newFieldRegistry[dataViewField.name];
}
});
if (test.compatibleControlTypes.length === 0) {
return { ...registry };
}
return { ...registry, [field.name]: test };
}, {});

return newFieldRegistry;
return fieldRegistry;
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@
font-style: italic;
}

.optionsList__loadMore {
font-style: italic;
}

.optionsList__negateLabel {
font-weight: bold;
font-size: $euiSizeM;
color: $euiColorDanger;
}

.optionsList__ignoredBadge {
.optionsList__actionBarFirstBadge {
margin-left: $euiSizeS;
}

Expand Down Expand Up @@ -86,3 +90,18 @@
.optionsList--sortPopover {
width: $euiSizeXL * 7;
}

.optionslist--loadingMoreGroupLabel {
text-align: center;
padding: $euiSizeM;
font-style: italic;
height: $euiSizeXXL !important;
}

.optionslist--endOfOptionsGroupLabel {
text-align: center;
font-size: $euiSizeM;
height: auto !important;
color: $euiTextSubduedColor;
padding: $euiSizeM;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { BehaviorSubject } from 'rxjs';
describe('Options list control', () => {
const defaultProps = {
typeaheadSubject: new BehaviorSubject(''),
loadMoreSubject: new BehaviorSubject(10),
};

interface MountOptions {
Expand Down
Loading