/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; import { i18n } from '@osd/i18n'; import { EuiFacetButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPopover, EuiPopoverFooter, EuiPopoverTitle, EuiSelect, EuiSwitch, EuiSwitchEvent, EuiForm, EuiFormRow, EuiButtonGroup, EuiOutsideClickDetector, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; export interface State { searchable: string; aggregatable: string; type: string; missing: boolean; [index: string]: string | boolean; } export interface Props { /** * triggered on input of user into search field */ onChange: (field: string, value: string | boolean | undefined) => void; /** * the input value of the user */ value?: string; /** * types for the type filter */ types: string[]; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ export function DiscoverFieldSearch({ onChange, value, types }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); const aggregatableLabel = i18n.translate('discover.fieldChooser.filter.aggregatableLabel', { defaultMessage: 'Aggregatable', }); const searchableLabel = i18n.translate('discover.fieldChooser.filter.searchableLabel', { defaultMessage: 'Searchable', }); const typeLabel = i18n.translate('discover.fieldChooser.filter.typeLabel', { defaultMessage: 'Type', }); const typeOptions = types ? types.map((type) => { return { value: type, text: type }; }) : [{ value: 'any', text: 'any' }]; const [activeFiltersCount, setActiveFiltersCount] = useState(0); const [isPopoverOpen, setPopoverOpen] = useState(false); const [values, setValues] = useState({ searchable: 'any', aggregatable: 'any', type: 'any', missing: true, }); if (typeof value !== 'string') { // at initial rendering value is undefined (angular related), this catches the warning // should be removed once all is react return null; } const filterBtnAriaLabel = isPopoverOpen ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { defaultMessage: 'Hide field filter settings', }) : i18n.translate('discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { defaultMessage: 'Show field filter settings', }); const handleFacetButtonClicked = () => { setPopoverOpen(!isPopoverOpen); }; const applyFilterValue = (id: string, filterValue: string | boolean) => { switch (filterValue) { case 'any': if (id !== 'type') { onChange(id, undefined); } else { onChange(id, filterValue); } break; case 'true': onChange(id, true); break; case 'false': onChange(id, false); break; default: onChange(id, filterValue); } }; const isFilterActive = (name: string, filterValue: string | boolean) => { return name !== 'missing' && filterValue !== 'any'; }; const handleValueChange = (name: string, filterValue: string | boolean) => { const previousValue = values[name]; updateFilterCount(name, previousValue, filterValue); const updatedValues = { ...values }; updatedValues[name] = filterValue; setValues(updatedValues); applyFilterValue(name, filterValue); }; const updateFilterCount = ( name: string, previousValue: string | boolean, currentValue: string | boolean ) => { const previouslyFilterActive = isFilterActive(name, previousValue); const filterActive = isFilterActive(name, currentValue); const diff = Number(filterActive) - Number(previouslyFilterActive); setActiveFiltersCount(activeFiltersCount + diff); }; const handleMissingChange = (e: EuiSwitchEvent) => { const missingValue = e.target.checked; handleValueChange('missing', missingValue); }; const buttonContent = ( } isSelected={activeFiltersCount > 0} quantity={activeFiltersCount} onClick={handleFacetButtonClicked} > ); const select = ( id: string, selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>, selectValue: string ) => { return ( ) => handleValueChange(id, e.target.value) } aria-label={i18n.translate('discover.fieldChooser.filter.fieldSelectorLabel', { defaultMessage: 'Selection of {id} filter options', values: { id }, })} data-test-subj={`${id}Select`} compressed /> ); }; const toggleButtons = (id: string) => { return [ { id: `${id}-any`, label: 'any', }, { id: `${id}-true`, label: 'yes', }, { id: `${id}-false`, label: 'no', }, ]; }; const buttonGroup = (id: string, legend: string) => { return ( handleValueChange(id, optionId.replace(`${id}-`, ''))} buttonSize="compressed" isFullWidth data-test-subj={`${id}ButtonGroup`} /> ); }; const selectionPanel = (
{buttonGroup('aggregatable', aggregatableLabel)} {buttonGroup('searchable', searchableLabel)} {select('type', typeOptions, values.type)}
); return ( onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} value={value} />
{}} isDisabled={!isPopoverOpen}> { setPopoverOpen(false); }} button={buttonContent} > {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', { defaultMessage: 'Filter by type', })} {selectionPanel}
); }