Skip to content

Commit

Permalink
[8.7] [Controls] Use EUI Selectable for Field search (elastic#151231) (
Browse files Browse the repository at this point in the history
…elastic#155454)

Closes elastic#155452

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Controls] Use EUI Selectable for Field search
(elastic#151231)](elastic#151231)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Devon
Thomson","email":"devon.thomson@elastic.co"},"sourceCommit":{"committedDate":"2023-02-24T15:39:15Z","message":"[Controls]
Use EUI Selectable for Field search (elastic#151231)\n\n## Summary\r\nReplaces
Control field selection list with
EUISelectable.","sha":"eb9cc11a7c81073a75fb3e6ec33e61796cc40aff","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Feature:Dashboard","Feature:Input
Control","Team:Presentation","loe:days","impact:low","backport:skip","v8.8.0"],"number":151231,"url":"https://github.com/elastic/kibana/pull/151231","mergeCommit":{"message":"[Controls]
Use EUI Selectable for Field search (elastic#151231)\n\n## Summary\r\nReplaces
Control field selection list with
EUISelectable.","sha":"eb9cc11a7c81073a75fb3e6ec33e61796cc40aff"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/151231","number":151231,"mergeCommit":{"message":"[Controls]
Use EUI Selectable for Field search (elastic#151231)\n\n## Summary\r\nReplaces
Control field selection list with
EUISelectable.","sha":"eb9cc11a7c81073a75fb3e6ec33e61796cc40aff"}}]}]
BACKPORT-->

---------

Co-authored-by: Devon Thomson <devon.thomson@elastic.co>
  • Loading branch information
Heenawter and ThomThomson authored Apr 20, 2023
1 parent f18982e commit cc16f95
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 277 deletions.
57 changes: 35 additions & 22 deletions src/plugins/controls/public/control_group/editor/control_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
* Side Public License, v 1.
*/

/*
* 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 React, { useEffect, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import useAsync from 'react-use/lib/useAsync';
Expand Down Expand Up @@ -87,7 +95,7 @@ export const ControlEditor = ({
controls: { getControlFactory },
} = pluginServices.getServices();
const [defaultTitle, setDefaultTitle] = useState<string>();
const [currentTitle, setCurrentTitle] = useState(title);
const [currentTitle, setCurrentTitle] = useState(title ?? '');
const [currentWidth, setCurrentWidth] = useState(width);
const [currentGrow, setCurrentGrow] = useState(grow);
const [controlEditorValid, setControlEditorValid] = useState(false);
Expand Down Expand Up @@ -120,6 +128,7 @@ export const ControlEditor = ({
});

const {
loading: dataViewLoading,
value: { selectedDataView, fieldRegistry } = {
selectedDataView: undefined,
fieldRegistry: undefined,
Expand All @@ -140,12 +149,12 @@ export const ControlEditor = ({
() => setControlEditorValid(Boolean(selectedField) && Boolean(selectedDataView)),
[selectedField, setControlEditorValid, selectedDataView]
);

const controlType =
selectedField && fieldRegistry && fieldRegistry[selectedField].compatibleControlTypes[0];
const factory = controlType && getControlFactory(controlType);
const CustomSettings =
factory && (factory as IEditableControlFactory).controlEditorOptionsComponent;

return (
<>
<EuiFlyoutHeader hasBorder>
Expand Down Expand Up @@ -178,25 +187,28 @@ export const ControlEditor = ({
selectableProps={{ isLoading: dataViewListLoading }}
/>
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => Boolean(fieldRegistry?.[field.name])}
selectedFieldName={selectedField}
dataView={selectedDataView}
onSelectField={(field) => {
onTypeEditorChange({
fieldName: field.name,
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
setSelectedField(field.name);
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
}}
/>
</EuiFormRow>
{fieldRegistry && (
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => Boolean(fieldRegistry[field.name])}
selectedFieldName={selectedField}
dataView={selectedDataView}
onSelectField={(field) => {
onTypeEditorChange({
fieldName: field.name,
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
setSelectedField(field.name);
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
}}
selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }}
/>
</EuiFormRow>
)}
<EuiFormRow label={ControlGroupStrings.manageControl.getControlTypeTitle()}>
{factory ? (
<EuiFlexGroup alignItems="center" gutterSize="xs">
Expand Down Expand Up @@ -224,6 +236,7 @@ export const ControlEditor = ({
}}
/>
</EuiFormRow>

<EuiFormRow label={ControlGroupStrings.manageControl.getWidthInputTitle()}>
<>
<EuiButtonGroup
Expand Down Expand Up @@ -258,7 +271,7 @@ export const ControlEditor = ({
<CustomSettings
onChange={onTypeEditorChange}
initialInput={embeddable?.getInput()}
fieldType={fieldRegistry[selectedField].field.type}
fieldType={fieldRegistry?.[selectedField].field.type}
/>
</EuiFormRow>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
.presFieldPicker__fieldButton {
background: $euiColorEmptyShade;
}

.presFieldPickerFieldButtonActive {
box-shadow: 0 0 0 2px $euiColorPrimary;
background-color: transparentize($euiColorPrimary, .9);
}

.presFieldPicker__fieldPanel {
height: 300px;
overflow-y: scroll;
}
.fieldPickerSelectable {
height: $euiSizeXXL * 9; // 40 * 9 = 360px

.presFieldPicker__container--disabled {
opacity: .7;
pointer-events: none;
&.fieldPickerSelectableLoading {
.euiSelectableMessage {
height: 100%;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@

import classNames from 'classnames';
import { sortBy, uniq } from 'lodash';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { FieldButton, FieldIcon } from '@kbn/react-field';
import React, { useEffect, useMemo, useState } from 'react';

import { i18n } from '@kbn/i18n';
import { FieldIcon } from '@kbn/react-field';
import {
EuiSpacer,
EuiFormRow,
EuiSelectable,
EuiSelectableOption,
EuiSelectableProps,
} from '@elastic/eui';
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { FieldSearch } from './field_search';

import { FieldTypeFilter } from './field_type_filter';

import './field_picker.scss';

Expand All @@ -23,123 +30,119 @@ export interface FieldPickerProps {
selectedFieldName?: string;
filterPredicate?: (f: DataViewField) => boolean;
onSelectField?: (selectedField: DataViewField) => void;
selectableProps?: Partial<EuiSelectableProps>;
}

export const FieldPicker = ({
dataView,
onSelectField,
filterPredicate,
selectableProps,
selectedFieldName,
}: FieldPickerProps) => {
const [nameFilter, setNameFilter] = useState<string>('');
const [typesFilter, setTypesFilter] = useState<string[]>([]);
const [fieldSelectableOptions, setFieldSelectableOptions] = useState<EuiSelectableOption[]>([]);

// Retrieve, filter, and sort fields from data view
const fields = dataView
? sortBy(
dataView.fields
.filter(
(f) =>
f.name.toLowerCase().includes(nameFilter.toLowerCase()) &&
(typesFilter.length === 0 || typesFilter.includes(f.type as string))
)
const availableFields = useMemo(
() =>
sortBy(
(dataView?.fields ?? [])
.filter((f) => typesFilter.length === 0 || typesFilter.includes(f.type as string))
.filter((f) => (filterPredicate ? filterPredicate(f) : true)),
['name']
)
: [];
),
[dataView, filterPredicate, typesFilter]
);

useEffect(() => {
if (!dataView) return;
const options: EuiSelectableOption[] = (availableFields ?? []).map((field) => {
return {
key: field.name,
label: field.displayName ?? field.name,
className: classNames('presFieldPicker__fieldButton', {
presFieldPickerFieldButtonActive: field.name === selectedFieldName,
}),
'data-test-subj': `field-picker-select-${field.name}`,
prepend: (
<FieldIcon
type={field.type}
label={field.name}
scripted={field.scripted}
className="eui-alignMiddle"
/>
),
};
});
setFieldSelectableOptions(options);
}, [availableFields, dataView, filterPredicate, selectedFieldName, typesFilter]);

const uniqueTypes = dataView
? uniq(
dataView.fields
.filter((f) => (filterPredicate ? filterPredicate(f) : true))
.map((f) => f.type as string)
)
: [];
const uniqueTypes = useMemo(
() =>
dataView
? uniq(
dataView.fields
.filter((f) => (filterPredicate ? filterPredicate(f) : true))
.map((f) => f.type as string)
)
: [],
[dataView, filterPredicate]
);

const fieldTypeFilter = (
<EuiFormRow fullWidth={true}>
<FieldTypeFilter
onFieldTypesChange={(types) => setTypesFilter(types)}
fieldTypesValue={typesFilter}
availableFieldTypes={uniqueTypes}
buttonProps={{ disabled: Boolean(selectableProps?.isLoading) }}
/>
</EuiFormRow>
);

return (
<EuiFlexGroup
direction="column"
alignItems="stretch"
gutterSize="s"
className={`presFieldPicker__container ${
!dataView && 'presFieldPicker__container--disabled'
}`}
<EuiSelectable
{...selectableProps}
className={classNames('fieldPickerSelectable', {
fieldPickerSelectableLoading: selectableProps?.isLoading,
})}
emptyMessage={i18n.translate('presentationUtil.fieldPicker.noFieldsLabel', {
defaultMessage: 'No matching fields',
})}
aria-label={i18n.translate('presentationUtil.fieldPicker.selectableAriaLabel', {
defaultMessage: 'Select a field',
})}
searchable
options={fieldSelectableOptions}
onChange={(options, _, changedOption) => {
setFieldSelectableOptions(options);
if (!dataView || !changedOption.key) return;
const field = dataView.getFieldByName(changedOption.key);
if (field) onSelectField?.(field);
}}
searchProps={{
'data-test-subj': 'field-search-input',
placeholder: i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', {
defaultMessage: 'Search field names',
}),
}}
listProps={{
isVirtualized: true,
showIcons: false,
bordered: true,
}}
height={300}
>
<EuiFlexItem grow={false}>
<FieldSearch
onSearchChange={(val) => setNameFilter(val)}
searchValue={nameFilter}
onFieldTypesChange={(types) => setTypesFilter(types)}
fieldTypesValue={typesFilter}
availableFieldTypes={uniqueTypes}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel
paddingSize="s"
hasShadow={false}
hasBorder={true}
className="presFieldPicker__fieldPanel"
>
{fields.length > 0 && (
<EuiFlexGroup direction="column" gutterSize="none">
{fields.map((f, i) => {
return (
<EuiFlexItem key={f.name}>
<FieldButton
data-test-subj={`field-picker-select-${f.name}`}
className={classNames('presFieldPicker__fieldButton', {
presFieldPickerFieldButtonActive: f.name === selectedFieldName,
})}
onClick={() => {
onSelectField?.(f);
}}
isActive={f.name === selectedFieldName}
fieldName={f.name}
fieldIcon={<FieldIcon type={f.type} label={f.name} scripted={f.scripted} />}
/>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
)}
{!dataView && (
<EuiFlexGroup
direction="column"
gutterSize="none"
alignItems="center"
justifyContent="center"
>
<EuiFlexItem>
<EuiText color="subdued">
<FormattedMessage
id="presentationUtil.fieldPicker.noDataViewLabel"
defaultMessage="No data view selected"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
{dataView && fields.length === 0 && (
<EuiFlexGroup
direction="column"
gutterSize="none"
alignItems="center"
justifyContent="center"
>
<EuiFlexItem>
<EuiText color="subdued">
<FormattedMessage
id="presentationUtil.fieldPicker.noFieldsLabel"
defaultMessage="No matching fields"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
{(list, search) => (
<>
{search}
<EuiSpacer size={'s'} />
{fieldTypeFilter}
<EuiSpacer size={'s'} />
{list}
</>
)}
</EuiSelectable>
);
};

Expand Down
Loading

0 comments on commit cc16f95

Please sign in to comment.