diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index 6b543ada20aec..6c2605710dd45 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -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'; @@ -87,7 +95,7 @@ export const ControlEditor = ({ controls: { getControlFactory }, } = pluginServices.getServices(); const [defaultTitle, setDefaultTitle] = useState(); - 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); @@ -120,6 +128,7 @@ export const ControlEditor = ({ }); const { + loading: dataViewLoading, value: { selectedDataView, fieldRegistry } = { selectedDataView: undefined, fieldRegistry: undefined, @@ -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 ( <> @@ -178,25 +187,28 @@ export const ControlEditor = ({ selectableProps={{ isLoading: dataViewListLoading }} /> - - 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); - } - }} - /> - + {fieldRegistry && ( + + 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 }} + /> + + )} {factory ? ( @@ -224,6 +236,7 @@ export const ControlEditor = ({ }} /> + <> )} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.scss b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss index a06c469e713bf..cb81739118249 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.scss +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss @@ -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%; + } + } } \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index 3c8a084f2686b..3dfaed00c6a75 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -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'; @@ -23,123 +30,119 @@ export interface FieldPickerProps { selectedFieldName?: string; filterPredicate?: (f: DataViewField) => boolean; onSelectField?: (selectedField: DataViewField) => void; + selectableProps?: Partial; } export const FieldPicker = ({ dataView, onSelectField, filterPredicate, + selectableProps, selectedFieldName, }: FieldPickerProps) => { - const [nameFilter, setNameFilter] = useState(''); const [typesFilter, setTypesFilter] = useState([]); + const [fieldSelectableOptions, setFieldSelectableOptions] = useState([]); - // 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: ( + + ), + }; + }); + 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 = ( + + setTypesFilter(types)} + fieldTypesValue={typesFilter} + availableFieldTypes={uniqueTypes} + buttonProps={{ disabled: Boolean(selectableProps?.isLoading) }} + /> + + ); return ( - { + 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} > - - setNameFilter(val)} - searchValue={nameFilter} - onFieldTypesChange={(types) => setTypesFilter(types)} - fieldTypesValue={typesFilter} - availableFieldTypes={uniqueTypes} - /> - - - - {fields.length > 0 && ( - - {fields.map((f, i) => { - return ( - - { - onSelectField?.(f); - }} - isActive={f.name === selectedFieldName} - fieldName={f.name} - fieldIcon={} - /> - - ); - })} - - )} - {!dataView && ( - - - - - - - - )} - {dataView && fields.length === 0 && ( - - - - - - - - )} - - - + {(list, search) => ( + <> + {search} + + {fieldTypeFilter} + + {list} + + )} + ); }; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx deleted file mode 100644 index d3307f71988f1..0000000000000 --- a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFieldSearch, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiOutsideClickDetector, - EuiFilterButton, - EuiSpacer, - EuiPopoverTitle, -} from '@elastic/eui'; -import { FieldIcon } from '@kbn/react-field'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import './field_search.scss'; - -export interface Props { - onSearchChange: (value: string) => void; - searchValue?: string; - - onFieldTypesChange: (value: string[]) => void; - fieldTypesValue: string[]; - - availableFieldTypes: string[]; -} - -export function FieldSearch({ - onSearchChange, - searchValue, - onFieldTypesChange, - fieldTypesValue, - availableFieldTypes, -}: Props) { - const searchPlaceholder = i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', { - defaultMessage: 'Search field names', - }); - - const [isPopoverOpen, setPopoverOpen] = useState(false); - - const handleFilterButtonClicked = () => { - setPopoverOpen(!isPopoverOpen); - }; - - const buttonContent = ( - 0} - numFilters={0} - hasActiveFilters={fieldTypesValue.length > 0} - numActiveFilters={fieldTypesValue.length} - onClick={handleFilterButtonClicked} - > - - - ); - - return ( - - - - onSearchChange(event.currentTarget.value)} - placeholder={searchPlaceholder} - value={searchValue} - /> - - - - {}} isDisabled={!isPopoverOpen}> - - { - setPopoverOpen(false); - }} - button={buttonContent} - > - - {i18n.translate('presentationUtil.fieldSearch.filterByTypeLabel', { - defaultMessage: 'Filter by type', - })} - - ( - { - if (fieldTypesValue.includes(type)) { - onFieldTypesChange(fieldTypesValue.filter((f) => f !== type)); - } else { - onFieldTypesChange([...fieldTypesValue, type]); - } - }} - > - - - - - {type} - - - ))} - /> - - - - - ); -} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.scss b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.scss similarity index 100% rename from src/plugins/presentation_util/public/components/field_picker/field_search.scss rename to src/plugins/presentation_util/public/components/field_picker/field_type_filter.scss diff --git a/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx new file mode 100644 index 0000000000000..a17739cf8ccbd --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx @@ -0,0 +1,110 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiOutsideClickDetector, + EuiFilterButton, + EuiPopoverTitle, + EuiFilterButtonProps, +} from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import './field_type_filter.scss'; + +export interface Props { + onFieldTypesChange: (value: string[]) => void; + fieldTypesValue: string[]; + availableFieldTypes: string[]; + buttonProps?: Partial; +} + +export function FieldTypeFilter({ + onFieldTypesChange, + fieldTypesValue, + availableFieldTypes, + buttonProps, +}: Props) { + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const handleFilterButtonClicked = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const buttonContent = ( + 0} + numFilters={0} + hasActiveFilters={fieldTypesValue.length > 0} + numActiveFilters={fieldTypesValue.length} + onClick={handleFilterButtonClicked} + > + + + ); + + return ( + {}} isDisabled={!isPopoverOpen}> + + { + setPopoverOpen(false); + }} + button={buttonContent} + > + + {i18n.translate('presentationUtil.fieldSearch.filterByTypeLabel', { + defaultMessage: 'Filter by type', + })} + + ( + { + if (fieldTypesValue.includes(type)) { + onFieldTypesChange(fieldTypesValue.filter((f) => f !== type)); + } else { + onFieldTypesChange([...fieldTypesValue, type]); + } + }} + > + + + + + {type} + + + ))} + /> + + + + ); +} diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index c3160e650c2a8..ff4ac5b67f804 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -576,7 +576,7 @@ export class DashboardPageControls extends FtrService { public async controlsEditorSetfield( fieldName: string, expectedType?: string, - shouldSearch: boolean = false + shouldSearch: boolean = true ) { this.log.debug(`Setting control field to ${fieldName}`); if (shouldSearch) { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4abc729b8e7fa..024fddae01827 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -4887,7 +4887,6 @@ "presentationUtil.solutionToolbar.quickButton.ariaButtonLabel": "Créer {createType}", "presentationUtil.dashboardPicker.searchDashboardPlaceholder": "Recherche dans les tableaux de bord…", "presentationUtil.dataViewPicker.changeDataViewTitle": "Vue de données", - "presentationUtil.fieldPicker.noDataViewLabel": "Aucune vue de données sélectionnée", "presentationUtil.fieldPicker.noFieldsLabel": "Aucun champ correspondant", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "Filtrer par type", "presentationUtil.fieldSearch.filterByTypeLabel": "Filtrer par type", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ebbf41e8345ea..e9d6170253725 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4888,7 +4888,6 @@ "presentationUtil.solutionToolbar.quickButton.ariaButtonLabel": "新しい{createType}の作成", "presentationUtil.dashboardPicker.searchDashboardPlaceholder": "ダッシュボードを検索...", "presentationUtil.dataViewPicker.changeDataViewTitle": "データビュー", - "presentationUtil.fieldPicker.noDataViewLabel": "データビューが選択されていません", "presentationUtil.fieldPicker.noFieldsLabel": "一致するがフィールドがありません", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "タイプでフィルタリング", "presentationUtil.fieldSearch.filterByTypeLabel": "タイプでフィルタリング", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 98a5fc8b11df3..795f17050cccb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4888,7 +4888,6 @@ "presentationUtil.solutionToolbar.quickButton.ariaButtonLabel": "创建新的 {createType}", "presentationUtil.dashboardPicker.searchDashboardPlaceholder": "搜索仪表板......", "presentationUtil.dataViewPicker.changeDataViewTitle": "数据视图", - "presentationUtil.fieldPicker.noDataViewLabel": "未选择数据视图", "presentationUtil.fieldPicker.noFieldsLabel": "无匹配字段", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "按类型筛选", "presentationUtil.fieldSearch.filterByTypeLabel": "按类型筛选", diff --git a/x-pack/test/accessibility/apps/dashboard_controls.ts b/x-pack/test/accessibility/apps/dashboard_controls.ts index 8560b65b5fd34..b53a25d543680 100644 --- a/x-pack/test/accessibility/apps/dashboard_controls.ts +++ b/x-pack/test/accessibility/apps/dashboard_controls.ts @@ -11,6 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'home', 'dashboardControls']); const browser = getService('browser'); @@ -56,7 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Options control panel & dashboard with options control', async () => { - await testSubjects.click('field-picker-select-OriginCityName'); + await PageObjects.dashboardControls.controlsEditorSetfield('OriginCityName'); await a11y.testAppSnapshot(); await testSubjects.click('control-editor-save'); await a11y.testAppSnapshot();