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

[Controls] Use EUI Selectable for Field search #151231

Merged
merged 9 commits into from
Feb 24, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const ControlEditor = ({
});

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 @@ -198,27 +198,27 @@ export const ControlEditor = ({
/>
</EuiFormRow>
)}
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => {
return Boolean(fieldRegistry?.[field.name]);
}}
selectedFieldName={selectedField}
dataView={dataView}
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={dataView}
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>
)}
<EuiFormRow label={ControlGroupStrings.manageControl.getControlTypeTitle()}>
{factory ? (
<EuiFlexGroup alignItems="center" gutterSize="xs">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
.presFieldPicker__fieldButton {
background: $euiColorEmptyShade;
}

.presFieldPickerFieldButtonActive {
box-shadow: 0 0 0 2px $euiColorPrimary;
}

.presFieldPicker__fieldPanel {
height: 300px;
overflow-y: scroll;
}

.presFieldPicker__container--disabled {
opacity: .7;
pointer-events: none;
background-color: transparentize($euiColorPrimary, .9);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@

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 { EuiSelectable, EuiSelectableOption, EuiSpacer } 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 @@ -31,115 +32,102 @@ export const FieldPicker = ({
filterPredicate,
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 = (
<FieldTypeFilter
onFieldTypesChange={(types) => setTypesFilter(types)}
fieldTypesValue={typesFilter}
availableFieldTypes={uniqueTypes}
/>
);

return (
<EuiFlexGroup
direction="column"
alignItems="stretch"
gutterSize="s"
className={`presFieldPicker__container ${
!dataView && 'presFieldPicker__container--disabled'
}`}
<EuiSelectable
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,
Comment on lines +115 to +116
Copy link
Contributor

Choose a reason for hiding this comment

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

Since there is often a scrollbar, I think that bordered: true looks a bit nicer?

EuiSelectableBorder

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on Hannah's suggestion, a border would be a nice addition

Copy link
Contributor Author

@ThomThomson ThomThomson Feb 23, 2023

Choose a reason for hiding this comment

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

I felt that the border looked a bit too noisy, but after seeing the screenshots - and knowing that y'all are down for the border - I'll add it!

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