From e3ead3ebb11a802553c96a5a3d8909f1fc305804 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 30 Jun 2022 15:23:25 +0200 Subject: [PATCH] change(ui) - search optimization and autocomplete improvements --- .../FilterAutoComplete.module.css | 126 +++---- .../FilterAutoComplete/FilterAutoComplete.tsx | 297 ++++++++-------- .../shared/Filters/FilterItem/FilterItem.tsx | 184 +++++----- .../shared/Filters/FilterList/FilterList.tsx | 2 +- .../Filters/FilterValue/FilterValue.tsx | 327 +++++++++--------- 5 files changed, 464 insertions(+), 472 deletions(-) diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.module.css b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.module.css index 51bbd55ce..55f2a4f0b 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.module.css +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.module.css @@ -1,81 +1,61 @@ .wrapper { - border: solid thin $gray-light !important; - border-radius: 3px; - border-radius: 3px; - display: flex; - align-items: center; - background-color: white; - width: 100%; - & input { - height: 24px; - font-size: 13px !important; - padding: 0 5px !important; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - border: solid thin transparent !important; - width: 100%; - } - - & .right { - height: 24px; + border: solid thin $gray-light !important; + border-radius: 3px; + background-color: white !important; display: flex; - align-items: stretch; - padding: 0; - background-color: $gray-lightest; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; + align-items: center; + height: 26px; + width: 100%; - & div { - /* background-color: red; */ - border-left: solid thin $gray-light !important; - width: 28px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - &:last-child { + & .right { + height: 24px; + display: flex; + align-items: stretch; + padding: 0; + background-color: $gray-lightest; border-top-right-radius: 3px; border-bottom-right-radius: 3px; - } - &:hover { - background-color: $gray-light; - } + margin-left: auto; + + & div { + /* background-color: red; */ + border-left: solid thin $gray-light !important; + width: 28px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + &:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + &:hover { + background-color: $gray-light; + } + } } - } } - -.menu { - border-radius: 0 0 3px 3px; - border: solid thin $gray-light !important; - box-shadow: 0 2px 2px 0 $gray-light; - min-height: 50px; - background-color: white; - max-height: 350px; - overflow-y: auto; - position: absolute; - top: 28px; - left: 0; - width: 400px; - z-index: 99; +.operatorDropdown { + font-weight: 400; + /* height: 30px; */ + min-width: 60px; + display: flex !important; + align-items: center; + justify-content: space-between; + padding: 0 8px !important; + font-size: 13px; + height: 26px; + /* background-color: rgba(255, 255, 255, 0.8) !important; */ + /* background-color: $gray-lightest !important; */ + /* border: solid thin rgba(34, 36, 38, 0.15) !important; */ + /* border-radius: 4px !important; */ + color: $gray-darkest !important; + font-size: 14px !important; + &.ui.basic.button { + box-shadow: 0 0 0 1px rgba(62, 170, 175, 36, 38, 0.35) inset, 0 0 0 0 rgba(62, 170, 175, 0.15) inset !important; + } + /* + & input { + padding: 0 8px !important; + } */ } - -.filterItem { - display: flex; - align-items: center; - padding: 8px 10px; - cursor: pointer; - border-radius: 3px; - /* transition: all 0.4s; */ - margin-bottom: 5px; - max-width: 100%; - & .label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &:hover { - background-color: $gray-lightest; - /* transition: all 0.2s; */ - } -} \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index 97699c7eb..af1fa74e3 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -1,155 +1,178 @@ import React, { useState, useEffect } from 'react'; -import { Icon, Loader } from 'UI'; +import { Icon } from 'UI'; import APIClient from 'App/api_client'; import { debounce } from 'App/utils'; import stl from './FilterAutoComplete.module.css'; -import cn from 'classnames'; +import { components, DropdownIndicatorProps } from 'react-select'; +import AsyncCreatableSelect from 'react-select/async-creatable'; -const hiddenStyle = { - whiteSpace: 'pre-wrap', - opacity: 0, position: 'fixed', left: '-3000px' +const dropdownStyles = { + control: (provided: any) => { + const obj = { + ...provided, + border: 'solid thin transparent !important', + backgroundColor: 'transparent', + cursor: 'pointer', + height: '26px', + minHeight: '26px', + borderRadius: '3px', + boxShadow: 'none !important', + }; + return obj; + }, + valueContainer: (provided: any) => ({ + ...provided, + // paddingRight: '0px', + width: 'fit-content', + alignItems: 'center', + height: '26px', + padding: '0 3px', + }), + // placeholder: (provided: any) => ({ + // ...provided, + // }), + indicatorsContainer: (provided: any) => ({ + ...provided, + padding: '0px', + height: '26px', + }), + option: (provided: any, state: any) => ({ + ...provided, + whiteSpace: 'nowrap', + }), + menu: (provided: any, state: any) => ({ + ...provided, + top: 20, + left: 0, + minWidth: 'fit-content', + overflow: 'hidden', + }), + container: (provided: any) => ({ + ...provided, + width: '100%', + }), + input: (provided: any) => ({ + ...provided, + height: '22px', + '& input:focus': { + border: 'none !important', + }, + }), + singleValue: (provided: any, state: { isDisabled: any }) => { + const opacity = state.isDisabled ? 0.5 : 1; + const transition = 'opacity 300ms'; + + return { + ...provided, + opacity, + transition, + display: 'flex', + alignItems: 'center', + height: '20px', + }; + }, }; interface Props { - showOrButton?: boolean; - showCloseButton?: boolean; - onRemoveValue?: () => void; - onAddValue?: () => void; - endpoint?: string; - method?: string; - params?: any; - headerText?: string; - placeholder?: string; - onSelect: (e: any, item: any) => void; - value: any; - icon?: string; + showOrButton?: boolean; + showCloseButton?: boolean; + onRemoveValue?: () => void; + onAddValue?: () => void; + endpoint?: string; + method?: string; + params?: any; + headerText?: string; + placeholder?: string; + onSelect: (e: any, item: any) => void; + value: any; + icon?: string; } function FilterAutoComplete(props: Props) { - const { - showCloseButton = false, - placeholder = 'Type to search', - method = 'GET', - showOrButton = false, - onRemoveValue = () => null, - onAddValue = () => null, - endpoint = '', - params = {}, - headerText = '', - value = '', - icon = null, - } = props; - const [showModal, setShowModal] = useState(false); - const [loading, setLoading] = useState(false) - const [options, setOptions] = useState([]); - const [query, setQuery] = useState(value); + const { + showCloseButton = false, + placeholder = 'Type to search', + method = 'GET', + showOrButton = false, + onRemoveValue = () => null, + onAddValue = () => null, + endpoint = '', + params = {}, + value = '', + } = props; + const [options, setOptions] = useState(value ? [{ label: value, value }] : []); + const [query, setQuery] = useState(value); - const requestValues = (q: any) => { - setLoading(true); + useEffect(() => { + setQuery(value); + }, [value, options]) - return new APIClient()[method?.toLocaleLowerCase()](endpoint, { ...params, q }) - .then((response: any) => { - if (response.ok) { - return response.json(); - } - throw new Error(response.statusText); - }) - .then(({ data }: any) => { - setOptions(data); - }) - .finally(() => setLoading(false)); - } - - const debouncedRequestValues = React.useCallback(debounce(requestValues, 1000), [params]); - - const onInputChange = ({ target: { value } }: any) => { - setQuery(value); - if (!showModal) { - setShowModal(true); - } - - if (value === '' || value === ' ') { - return - } - debouncedRequestValues(value); - } - - useEffect(() => { - setQuery(value); - }, [value]) - - const onBlur = (e) => { - setTimeout(() => { setShowModal(false) }, 200) - if (query !== value) { - props.onSelect(e, { value: query }) - } - } - - const onItemClick = (e: any, item: any) => { - e.stopPropagation(); - e.preventDefault(); - - if (query !== item.value) { - setQuery(item.value); - } - - props.onSelect(e, item); - } - - return ( -
-
- { - // const text = e.clipboardData.getData('Text'); - // // this.hiddenInput.value = text; - // // pasted = true; // to use only the hidden input - // } } - /> -
- { showCloseButton &&
} - { showOrButton &&
or
} -
-
- - { !showOrButton &&
or
} - - { showModal && ( -
- - { options.length === 0 ? ( -
No results found!
- ) : ( -
- { - options.map((item: any, i: any) => ( -
onItemClick(e, item) } - > - { icon && } - { item.value } -
- )) + const loadOptions = (inputValue: string, callback: (options: []) => void) => { + new APIClient() + [method?.toLocaleLowerCase()](endpoint, { ...params, q: inputValue }) + .then((response: any) => { + if (response.ok) { + return response.json(); } -
- )} -
+ throw new Error(response.statusText); + }) + .then(({ data }: any) => { + const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || []; + setOptions(_options); + callback(_options); + }) + }; + + const debouncedLoadOptions = React.useCallback(debounce(loadOptions, 1000), [params]); + + const handleInputChange = (newValue: string) => { + const inputValue = newValue.replace(/\W/g, ''); + setQuery(inputValue); + return inputValue; + }; + + return ( +
+
+ props.onSelect(null, obj)} + styles={dropdownStyles} + placeholder={placeholder} + value={value ? options.find((i: any) => i.value === query) : null} + components={{ + IndicatorSeparator: () => null, + DropdownIndicator, + }} + /> +
+ {showCloseButton && ( +
+ +
+ )} + {showOrButton && ( +
+ or +
+ )} +
+
+ + {!showOrButton &&
or
}
- )} -
- ); + ); } export default FilterAutoComplete; + +const DropdownIndicator = (props: DropdownIndicatorProps) => { + return ( + + + + ); +}; diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 1212f1f6c..154e862a7 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -8,107 +8,107 @@ import { FilterKey, FilterType } from 'App/types/filter/filterType'; import SubFilterItem from '../SubFilterItem'; interface Props { - filterIndex: number; - filter: any; // event/filter - onUpdate: (filter) => void; - onRemoveFilter: () => void; - isFilter?: boolean; - saveRequestPayloads?: boolean; + filterIndex: number; + filter: any; // event/filter + onUpdate: (filter) => void; + onRemoveFilter: () => void; + isFilter?: boolean; + saveRequestPayloads?: boolean; } function FilterItem(props: Props) { - const { isFilter = false, filterIndex, filter, saveRequestPayloads } = props; - const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny" || filter.operator === "isUndefined"); - const isSubFilter = filter.type === FilterType.SUB_FILTERS; + const { isFilter = false, filterIndex, filter, saveRequestPayloads } = props; + const canShowValues = !(filter.operator === 'isAny' || filter.operator === 'onAny' || filter.operator === 'isUndefined'); + const isSubFilter = filter.type === FilterType.SUB_FILTERS; - const replaceFilter = (filter) => { - props.onUpdate({ - ...filter, - value: [""], - filters: filter.filters ? filter.filters.map(i => ({ ...i, value: [""] })) : [] - }); - }; + const replaceFilter = (filter) => { + props.onUpdate({ + ...filter, + value: [''], + filters: filter.filters ? filter.filters.map((i) => ({ ...i, value: [''] })) : [], + }); + }; - const onOperatorChange = (e, { name, value }) => { - props.onUpdate({ ...filter, operator: value.value }) - } - - const onSourceOperatorChange = (e, { name, value }) => { - props.onUpdate({ ...filter, sourceOperator: value.value }) - } + const onOperatorChange = (e, { name, value }) => { + props.onUpdate({ ...filter, operator: value.value }); + }; - const onUpdateSubFilter = (subFilter, subFilterIndex) => { - props.onUpdate({ - ...filter, - filters: filter.filters.map((i, index) => { - if (index === subFilterIndex) { - return subFilter; - } - return i; - }) - }); - }; + const onSourceOperatorChange = (e, { name, value }) => { + props.onUpdate({ ...filter, sourceOperator: value.value }); + }; + const onUpdateSubFilter = (subFilter, subFilterIndex) => { + props.onUpdate({ + ...filter, + filters: filter.filters.map((i, index) => { + if (index === subFilterIndex) { + return subFilter; + } + return i; + }), + }); + }; - return ( -
-
- { !isFilter &&
- {filterIndex+1} -
} - - - {/* Filter with Source */} - { filter.hasSource && ( - <> - - - - )} + return ( +
+
+ {!isFilter && ( +
+ {filterIndex + 1} +
+ )} + - {/* Filter values */} - { !isSubFilter && ( - <> - - { canShowValues && () } - - )} + {/* Filter with Source */} + {filter.hasSource && ( + <> + + + + )} - {/* filters */} - {isSubFilter && ( -
- {filter.filters.filter(i => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads).map((subFilter, subFilterIndex) => ( - onUpdateSubFilter(f, subFilterIndex)} - onRemoveFilter={props.onRemoveFilter} - /> - ))} -
- )} -
-
-
- + {/* Filter values */} + {!isSubFilter && ( + <> + + {canShowValues && } + + )} + + {/* filters */} + {isSubFilter && ( +
+ {filter.filters + .filter((i) => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads) + .map((subFilter, subFilterIndex) => ( + onUpdateSubFilter(f, subFilterIndex)} + onRemoveFilter={props.onRemoveFilter} + /> + ))} +
+ )} +
+
+
+ +
+
-
-
- ); + ); } -export default FilterItem; \ No newline at end of file +export default FilterItem; diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx index 01388946b..1116c044f 100644 --- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -59,7 +59,7 @@ function FilterList(props: Props) {
{filters.map((filter: any, filterIndex: any) => filter.isEvent ? ( props.onUpdateFilter(filterIndex, filter)} diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 7087926ff..b8cff4ecd 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -5,191 +5,180 @@ import { FilterKey, FilterCategory, FilterType } from 'Types/filter/filterType'; import FilterValueDropdown from '../FilterValueDropdown'; import FilterDuration from '../FilterDuration'; import { debounce } from 'App/utils'; -import { assist as assistRoute, isRoute } from "App/routes"; +import { assist as assistRoute, isRoute } from 'App/routes'; const ASSIST_ROUTE = assistRoute(); interface Props { - filter: any; - onUpdate: (filter: any) => void; + filter: any; + onUpdate: (filter: any) => void; } function FilterValue(props: Props) { - const { filter } = props; - const [durationValues, setDurationValues] = useState({ minDuration: filter.value[0], maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0] }); - const showCloseButton = filter.value.length > 1; - const lastIndex = filter.value.length - 1; + const { filter } = props; + const [durationValues, setDurationValues] = useState({ + minDuration: filter.value[0], + maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0], + }); + const showCloseButton = filter.value.length > 1; + const lastIndex = filter.value.length - 1; - const onAddValue = () => { - const newValue = filter.value.concat(''); - props.onUpdate({ ...filter, value: newValue }); - } + const onAddValue = () => { + const newValue = filter.value.concat(''); + props.onUpdate({ ...filter, value: newValue }); + }; - const onRemoveValue = (valueIndex: any) => { - const newValue = filter.value.filter((_: any, index: any) => index !== valueIndex); - props.onUpdate({ ...filter, value: newValue }); - } + const onRemoveValue = (valueIndex: any) => { + const newValue = filter.value.filter((_: any, index: any) => index !== valueIndex); + props.onUpdate({ ...filter, value: newValue }); + }; - const onChange = (e: any, item: any, valueIndex: any) => { - const newValues = filter.value.map((_: any, _index: any) => { - if (_index === valueIndex) { - return item.value; - } - return _; - }) - props.onUpdate({ ...filter, value: newValues }) - } + const onChange = (e: any, item: any, valueIndex: any) => { + const newValues = filter.value.map((_: any, _index: any) => { + if (_index === valueIndex) { + return item.value; + } + return _; + }); + props.onUpdate({ ...filter, value: newValues }); + }; - const debounceOnSelect = React.useCallback(debounce(onChange, 500), [onChange]); + const debounceOnSelect = React.useCallback(debounce(onChange, 500), [onChange]); - const onDurationChange = (newValues: any) => { - setDurationValues({ ...durationValues, ...newValues }); - } + const onDurationChange = (newValues: any) => { + setDurationValues({ ...durationValues, ...newValues }); + }; - const handleBlur = (e: any) => { - if (filter.type === FilterType.DURATION) { - const { maxDuration, minDuration, key } = filter; - if (maxDuration || minDuration) return; - if (maxDuration !== durationValues.maxDuration || - minDuration !== durationValues.minDuration) { - props.onUpdate({ ...filter, value: [durationValues.minDuration, durationValues.maxDuration] }); - } - } - } + const handleBlur = (e: any) => { + if (filter.type === FilterType.DURATION) { + const { maxDuration, minDuration, key } = filter; + if (maxDuration || minDuration) return; + if (maxDuration !== durationValues.maxDuration || minDuration !== durationValues.minDuration) { + props.onUpdate({ ...filter, value: [durationValues.minDuration, durationValues.maxDuration] }); + } + } + }; - const getParms = (key: any) => { - let params: any = { type: filter.key }; - switch (filter.category) { - case FilterCategory.METADATA: - params = { type: FilterKey.METADATA, key: key }; - } + const getParms = (key: any) => { + let params: any = { type: filter.key }; + switch (filter.category) { + case FilterCategory.METADATA: + params = { type: FilterKey.METADATA, key: key }; + } - if (isRoute(ASSIST_ROUTE, window.location.pathname)) { - params = { ...params, live: true }; - } + if (isRoute(ASSIST_ROUTE, window.location.pathname)) { + params = { ...params, live: true }; + } - return params; - } + return params; + }; - const renderValueFiled = (value: any, valueIndex: any) => { - const showOrButton = valueIndex === lastIndex && filter.type !== FilterType.NUMBER; - switch(filter.type) { - case FilterType.STRING: - return ( - onRemoveValue(valueIndex)} - onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} - icon={filter.icon} - /> - ) - case FilterType.DROPDOWN: - return ( - onChange(null, { value }, valueIndex)} - /> - ) - case FilterType.ISSUE: - case FilterType.MULTIPLE_DROPDOWN: - return ( - onChange(null, { value }, valueIndex)} - onAddValue={onAddValue} - onRemoveValue={() => onRemoveValue(valueIndex)} - showCloseButton={showCloseButton} - showOrButton={showOrButton} - /> - ) - case FilterType.DURATION: - return ( - - ) - case FilterType.NUMBER_MULTIPLE: - return ( - onRemoveValue(valueIndex)} - onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} - icon={filter.icon} - type="number" - /> - ) - case FilterType.NUMBER: - return ( - onRemoveValue(valueIndex)} - onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} - icon={filter.icon} - type="number" - allowDecimals={false} - isMultilple={false} - /> - // onChange(e, { value: e.target.value }, valueIndex)} - // /> - ) - case FilterType.MULTIPLE: - return ( - onRemoveValue(valueIndex)} - method={'GET'} - endpoint='/events/search' - params={getParms(filter.key)} - headerText={''} - // placeholder={''} - onSelect={(e, item) => onChange(e, item, valueIndex)} - icon={filter.icon} - /> - ) - } - } + const renderValueFiled = (value: any, valueIndex: any) => { + const showOrButton = valueIndex === lastIndex && filter.type !== FilterType.NUMBER; + switch (filter.type) { + case FilterType.STRING: + return ( + onRemoveValue(valueIndex)} + onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} + icon={filter.icon} + /> + ); + case FilterType.DROPDOWN: + return ( + onChange(null, { value }, valueIndex)} + /> + ); + case FilterType.ISSUE: + case FilterType.MULTIPLE_DROPDOWN: + return ( + onChange(null, { value }, valueIndex)} + onAddValue={onAddValue} + onRemoveValue={() => onRemoveValue(valueIndex)} + showCloseButton={showCloseButton} + showOrButton={showOrButton} + /> + ); + case FilterType.DURATION: + return ( + + ); + case FilterType.NUMBER_MULTIPLE: + return ( + onRemoveValue(valueIndex)} + onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} + icon={filter.icon} + type="number" + /> + ); + case FilterType.NUMBER: + return ( + onRemoveValue(valueIndex)} + onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)} + icon={filter.icon} + type="number" + allowDecimals={false} + isMultilple={false} + /> + ); + case FilterType.MULTIPLE: + return ( + onRemoveValue(valueIndex)} + method={'GET'} + endpoint="/events/search" + params={getParms(filter.key)} + headerText={''} + // placeholder={''} + onSelect={(e, item) => onChange(e, item, valueIndex)} + icon={filter.icon} + /> + ); + } + }; - return ( -
- { filter.type === FilterType.DURATION ? ( - renderValueFiled(filter.value, 0) - ) : ( - filter.value && filter.value.map((value: any, valueIndex: any) => ( -
- {renderValueFiled(value, valueIndex)} -
- )) - )} -
- ); + return ( +
+ {filter.type === FilterType.DURATION + ? renderValueFiled(filter.value, 0) + : filter.value && + filter.value.map((value: any, valueIndex: any) =>
{renderValueFiled(value, valueIndex)}
)} +
+ ); } -export default FilterValue; \ No newline at end of file +export default FilterValue;