From 596c5e373f942f7eb1e86c118f853b1b0ad94c7a Mon Sep 17 00:00:00 2001 From: Alexis Demetriou Date: Tue, 27 Aug 2024 11:04:10 +0300 Subject: [PATCH 1/3] Implemented filtering for each field of the connectors grid. --- frontend/src/components/Connect/List/List.tsx | 73 +++++- .../src/components/Connect/List/ListPage.tsx | 5 - .../components/common/Input/Input.styled.ts | 2 + .../common/MultiSearch/MultiSearch.styled.ts | 130 ++++++++++ .../common/MultiSearch/MultiSearch.tsx | 115 +++++++++ .../src/components/common/NewTable/Table.tsx | 231 +++++++++++++++--- .../src/components/common/Search/Search.tsx | 4 +- .../SearchAutocomplete.styled.ts | 141 +++++++++++ .../SearchAutocomplete/SearchAutocomplete.tsx | 156 ++++++++++++ 9 files changed, 808 insertions(+), 49 deletions(-) create mode 100644 frontend/src/components/common/MultiSearch/MultiSearch.styled.ts create mode 100644 frontend/src/components/common/MultiSearch/MultiSearch.tsx create mode 100644 frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.styled.ts create mode 100644 frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.tsx diff --git a/frontend/src/components/Connect/List/List.tsx b/frontend/src/components/Connect/List/List.tsx index 87b4d56db..ba1ea16ea 100644 --- a/frontend/src/components/Connect/List/List.tsx +++ b/frontend/src/components/Connect/List/List.tsx @@ -2,10 +2,14 @@ import React from 'react'; import useAppParams from 'lib/hooks/useAppParams'; import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths'; import Table, { TagCell } from 'components/common/NewTable'; -import { FullConnectorInfo } from 'generated-sources'; +import { + ConnectorState, + ConnectorType, + FullConnectorInfo, +} from 'generated-sources'; import { useConnectors } from 'lib/hooks/api/kafkaConnect'; import { ColumnDef } from '@tanstack/react-table'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import ActionsCell from './ActionsCell'; import TopicsCell from './TopicsCell'; @@ -14,11 +18,7 @@ import RunningTasksCell from './RunningTasksCell'; const List: React.FC = () => { const navigate = useNavigate(); const { clusterName } = useAppParams(); - const [searchParams] = useSearchParams(); - const { data: connectors } = useConnectors( - clusterName, - searchParams.get('q') || '' - ); + const { data: connectors } = useConnectors(clusterName); const columns = React.useMemo[]>( () => [ @@ -34,6 +34,63 @@ const List: React.FC = () => { [] ); + const connectorTypeOptions = Object.values(ConnectorType).map((state) => ({ + label: state, + value: state, + })); + + const connectorStateOptions = Object.values(ConnectorState).map((state) => ({ + label: state, + value: state, + })); + + const columnSearchPlaceholders: { + id: string; + columnName: string; + placeholder: string; + type: string; + options?: { label: string; value: string }[]; + }[] = [ + { + id: 'name', + columnName: 'name', + placeholder: 'Search by Name', + type: 'input', + }, + { + id: 'connect', + columnName: 'connect', + placeholder: 'Search by Connect', + type: 'input', + }, + { + id: 'type', + columnName: 'type', + placeholder: 'Select Type', + type: 'autocomplete', + options: connectorTypeOptions, + }, + { + id: 'connectorClass', + columnName: 'connectorClass', + placeholder: 'Search by Plugin', + type: 'input', + }, + { + id: 'Topics', + columnName: 'topics', + placeholder: 'Search by Topics', + type: 'multiInput', + }, + { + id: 'status_state', + columnName: 'status', + placeholder: 'Select Status', + type: 'autocomplete', + options: connectorStateOptions, + }, + ]; + return ( { } emptyMessage="No connectors found" setRowId={(originalRow) => `${originalRow.name}-${originalRow.connect}`} + enableColumnSearch + columnSearchPlaceholders={columnSearchPlaceholders} /> ); }; diff --git a/frontend/src/components/Connect/List/ListPage.tsx b/frontend/src/components/Connect/List/ListPage.tsx index 5d61ff3c4..e7c45f159 100644 --- a/frontend/src/components/Connect/List/ListPage.tsx +++ b/frontend/src/components/Connect/List/ListPage.tsx @@ -2,11 +2,9 @@ import React, { Suspense } from 'react'; import useAppParams from 'lib/hooks/useAppParams'; import { ClusterNameRoute, clusterConnectorNewRelativePath } from 'lib/paths'; import ClusterContext from 'components/contexts/ClusterContext'; -import Search from 'components/common/Search/Search'; import * as Metrics from 'components/common/Metrics'; import PageHeading from 'components/common/PageHeading/PageHeading'; import Tooltip from 'components/common/Tooltip/Tooltip'; -import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { ConnectorState, Action, ResourceType } from 'generated-sources'; import { useConnectors, useConnects } from 'lib/hooks/api/kafkaConnect'; @@ -81,9 +79,6 @@ const ListPage: React.FC = () => { - - - }> diff --git a/frontend/src/components/common/Input/Input.styled.ts b/frontend/src/components/common/Input/Input.styled.ts index 2ff36c230..0344dc053 100644 --- a/frontend/src/components/common/Input/Input.styled.ts +++ b/frontend/src/components/common/Input/Input.styled.ts @@ -13,6 +13,7 @@ const INPUT_SIZES = { export const Wrapper = styled.div` position: relative; + min-width: 200px; &:hover { svg:first-child { fill: ${({ theme }) => theme.input.icon.hover}; @@ -52,6 +53,7 @@ export const Input = styled.input( : '40px'}; width: 100%; padding-left: ${search ? '36px' : '12px'}; + padding-right: 30px; font-size: 14px; &::placeholder { diff --git a/frontend/src/components/common/MultiSearch/MultiSearch.styled.ts b/frontend/src/components/common/MultiSearch/MultiSearch.styled.ts new file mode 100644 index 000000000..722720677 --- /dev/null +++ b/frontend/src/components/common/MultiSearch/MultiSearch.styled.ts @@ -0,0 +1,130 @@ +import styled, { css } from 'styled-components'; +import { ComponentProps } from 'react'; + +export interface InputProps extends ComponentProps<'input'> { + values?: string[]; + inputSize?: 'S' | 'M' | 'L'; + searchIcon?: boolean; + isFocused?: boolean; +} + +const INPUT_SIZES = { + S: 18, + M: 32, + L: 40, +}; + +export const Wrapper = styled.div( + ({ theme: { input } }) => css` + position: relative; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + border: 1px solid ${input.borderColor.normal}; + padding: 6px 32px 6px 12px; + border-radius: 4px; + min-width: 250px; + background-color: ${input.backgroundColor.normal}; + + &::placeholder { + color: ${input.color.placeholder.normal}; + font-size: 14px; + } + + &:hover { + border-color: ${input.borderColor.hover}; + } + + &:focus { + outline: none; + border-color: ${input.borderColor.focus}; + &::placeholder { + color: transparent; + } + } + ` +); + +export const ValuesContainer = styled.div( + ({ isFocused }) => css` + display: flex; + align-items: center; + flex-wrap: ${isFocused ? 'nowrap' : 'wrap'}; + gap: 8px; + flex-grow: 1; + ` +); + +export const Tag = styled.div` + display: flex; + align-items: center; + background-color: ${({ theme }) => theme.tag.backgroundColor.blue}; + color: ${({ theme }) => theme.tag.color}; + border-radius: 12px; + padding: 2px 0 2px 8px; + font-size: 12px; +`; + +export const RemoveButton = styled.button` + background: none; + border: none; + margin-left: 2px; + display: flex; + align-items: center; + cursor: pointer; + color: ${({ theme }) => theme.tag.color}; + + &:hover { + color: ${({ theme }) => theme.tag.backgroundColor}; + } +`; + +export const Input = styled.input( + ({ theme: { input }, inputSize, values }) => css` + flex-grow: 1; + background-color: ${input.backgroundColor.normal}; + border: none; + color: ${input.color.normal}; + height: ${inputSize ? `${INPUT_SIZES[inputSize]}px` : '32px'}; + font-size: 14px; + box-sizing: border-box; + width: ${values ? `15px` : '32px'}; + + &::placeholder { + color: ${input.color.placeholder.normal}; + font-size: 14px; + } + + &:focus { + outline: none; + &::placeholder { + color: transparent; + } + } + ` +); + +export const IconButtonWrapper = styled.span.attrs(() => ({ + role: 'button', + tabIndex: 0, +}))` + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + &:hover { + cursor: pointer; + } +`; + +export const RemainingTagCount = styled.span` + font-size: 14px; + color: ${({ theme }) => theme.input.color.normal}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/frontend/src/components/common/MultiSearch/MultiSearch.tsx b/frontend/src/components/common/MultiSearch/MultiSearch.tsx new file mode 100644 index 000000000..a07b8d900 --- /dev/null +++ b/frontend/src/components/common/MultiSearch/MultiSearch.tsx @@ -0,0 +1,115 @@ +import React, { useRef, useState } from 'react'; +import useClickOutside from 'lib/hooks/useClickOutside'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; +import SearchIcon from 'components/common/Icons/SearchIcon'; + +import * as S from './MultiSearch.styled'; + +export interface MultiSearchProps extends Omit { + name: string; + value?: string; + values?: string[]; + onChange?: (value: string, values: string[]) => void; + inputSize?: 'S' | 'M' | 'L'; + placeholder?: string; + searchIcon?: boolean; +} + +const MAX_VISIBLE_TAGS = 1; + +const MultiSearch: React.FC = ({ + name, + value = '', + values = [], + onChange, + inputSize = 'S', + placeholder = '', + searchIcon = true, + ...rest +}) => { + const [inputValue, setInputValue] = useState(value); + const [showAllTags, setShowAllTags] = useState(false); + + const selectContainerRef = useRef(null); + + const handleKeyEnter = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && inputValue.trim()) { + const trimmedValue = inputValue.trim(); + if (!values.includes(trimmedValue)) { + const newValues = [...values, trimmedValue]; + if (onChange) { + onChange('', newValues); + } + } + setInputValue(''); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + if (onChange) { + onChange(newValue, values); + } + }; + + const handleRemove = (valueToRemove: string) => { + const newValues = values.filter((tagValue) => tagValue !== valueToRemove); + if (onChange) { + onChange(inputValue, newValues); + } + }; + + const clearAll = () => { + if (onChange) { + onChange('', []); + } + }; + + const handleFocus = () => { + setShowAllTags(true); + }; + + const clickOutsideHandler = () => { + setShowAllTags(false); + }; + useClickOutside(selectContainerRef, clickOutsideHandler); + + const visibleTags = showAllTags ? values : values.slice(0, MAX_VISIBLE_TAGS); + const remainingTagsCount = values.length - MAX_VISIBLE_TAGS; + + return ( + + {searchIcon && } + + {visibleTags.map((tagValue) => ( + + {tagValue} + handleRemove(tagValue)}> + + + + ))} + {!showAllTags && remainingTagsCount > 0 && ( + +{remainingTagsCount} + )} + + + + + + + ); +}; + +export default MultiSearch; diff --git a/frontend/src/components/common/NewTable/Table.tsx b/frontend/src/components/common/NewTable/Table.tsx index df4421c98..513ec0f17 100644 --- a/frontend/src/components/common/NewTable/Table.tsx +++ b/frontend/src/components/common/NewTable/Table.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import type { ColumnDef, OnChangeFn, @@ -18,6 +18,9 @@ import { useLocation, useSearchParams } from 'react-router-dom'; import { PER_PAGE } from 'lib/constants'; import { Button } from 'components/common/Button/Button'; import Input from 'components/common/Input/Input'; +import Search from 'components/common/Search/Search'; +import SearchAutocomplete from 'components/common/SearchAutocomplete/SearchAutocomplete'; +import MultiSearch from 'components/common/MultiSearch/MultiSearch'; import * as S from './Table.styled'; import updateSortingState from './utils/updateSortingState'; @@ -60,6 +63,14 @@ export interface TableProps { onMouseLeave?: () => void; setRowId?: (originalRow: TData) => string; + enableColumnSearch?: boolean; + columnSearchPlaceholders?: { + id: string; + columnName: string; + placeholder: string; + type: string; + options?: { label: string; value: string }[]; + }[]; } type UpdaterFn = (previousState: T) => T; @@ -137,10 +148,51 @@ function Table({ onRowHover, onMouseLeave, setRowId, + enableColumnSearch = false, + columnSearchPlaceholders = [], }: TableProps) { const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); - const [rowSelection, setRowSelection] = React.useState({}); + const [rowSelection, setRowSelection] = useState({}); + + const [searchState, setSearchState] = useState< + Record + >( + columnSearchPlaceholders.reduce( + (acc, curr) => { + acc[curr.id] = curr.type === 'input' ? '' : undefined; + return acc; + }, + {} as Record + ) + ); + + const [multiSearchState, setMultiSearchState] = useState< + Record + >( + columnSearchPlaceholders.reduce( + (acc, curr) => { + acc[curr.id] = curr.type === 'input' ? [] : undefined; + return acc; + }, + {} as Record + ) + ); + + const handleSearchChange = (id: string, value: string) => { + setSearchState((prevState) => ({ + ...prevState, + [id]: value, + })); + }; + + const handleMultiSearchChange = (id: string, values: string[]) => { + setMultiSearchState((prevState) => ({ + ...prevState, + [id]: values, + })); + }; + const onSortingChange = React.useCallback( (updater: UpdaterFn) => { const newState = updateSortingState(updater, searchParams); @@ -159,8 +211,52 @@ function Table({ [searchParams, location] ); + const filteredConnectors = data?.filter((connector) => { + return columnSearchPlaceholders.every((placeholder) => { + const searchValue = searchState[placeholder.id]; + const multiSearchValues = multiSearchState[placeholder.id]; + + const connectorValue = + connector[placeholder.columnName as keyof typeof connector]; + + if (multiSearchValues && multiSearchValues.length > 0) { + const matchesAllTags = multiSearchValues.every((value) => + Array.isArray(connectorValue) + ? connectorValue.includes(value) + : connectorValue?.toString() === value + ); + + if (!matchesAllTags) { + return false; + } + } + + if (searchValue) { + if (Array.isArray(connectorValue)) { + return connectorValue.some((item: string) => + item.toLowerCase().includes(searchValue.toLowerCase()) + ); + } + + if (typeof connectorValue === 'object' && connectorValue !== null) { + return Object.values(connectorValue as Record).some( + (item) => + item?.toString().toLowerCase().includes(searchValue.toLowerCase()) + ); + } + + return connectorValue + ?.toString() + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + return true; + }); + }); + const table = useReactTable({ - data, + data: filteredConnectors, pageCount, columns, state: { @@ -242,41 +338,104 @@ function Table({ {table.getHeaderGroups().map((headerGroup) => ( - - {!!enableRowSelection && ( - - {flexRender( - SelectRowHeader, - headerGroup.headers[0].getContext() - )} - - )} - {table.getCanSomeRowsExpand() && ( - - )} - {headerGroup.headers.map((header) => ( - -
+ +
+ {!!enableRowSelection && ( + {flexRender( - header.column.columnDef.header, - header.getContext() + SelectRowHeader, + headerGroup.headers[0].getContext() )} - - - ))} - + + )} + {table.getCanSomeRowsExpand() && ( + + )} + {headerGroup.headers.map((header) => ( + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+
+ ))} + + {enableColumnSearch && ( +
+ {!!enableRowSelection && } + {table.getCanSomeRowsExpand() && } + {headerGroup.headers.map((header) => { + const placeholderObj = columnSearchPlaceholders?.find( + (placeholder) => placeholder.id === header.column.id + ); + + if (placeholderObj) { + return ( + + {placeholderObj.type === 'autocomplete' && ( + { + handleSearchChange(placeholderObj.id, value); + table.setPageIndex(0); + }} + placeholder={placeholderObj.placeholder} + options={placeholderObj.options || []} + searchIcon={false} + /> + )} + {placeholderObj.type === 'multiInput' && ( + { + handleMultiSearchChange( + placeholderObj.id, + values + ); + handleSearchChange(placeholderObj.id, value); + table.setPageIndex(0); + }} + placeholder={placeholderObj.placeholder} + searchIcon={false} + /> + )} + {placeholderObj.type === 'input' && ( + { + handleSearchChange(placeholderObj.id, value); + table.setPageIndex(0); + }} + placeholder={placeholderObj.placeholder} + searchIcon={false} + /> + )} + + ); + } + return ; + })} + + )} + ))} diff --git a/frontend/src/components/common/Search/Search.tsx b/frontend/src/components/common/Search/Search.tsx index d62a1d0e6..4bbaae40a 100644 --- a/frontend/src/components/common/Search/Search.tsx +++ b/frontend/src/components/common/Search/Search.tsx @@ -10,6 +10,7 @@ interface SearchProps { disabled?: boolean; onChange?: (value: string) => void; value?: string; + searchIcon?: boolean; } const IconButtonWrapper = styled.span.attrs(() => ({ @@ -27,6 +28,7 @@ const Search: React.FC = ({ disabled = false, value, onChange, + searchIcon = true, }) => { const [searchParams, setSearchParams] = useSearchParams(); const ref = useRef>(null); @@ -70,7 +72,7 @@ const Search: React.FC = ({ inputSize="M" disabled={disabled} ref={ref} - search + search={searchIcon} clearIcon={ diff --git a/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.styled.ts b/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.styled.ts new file mode 100644 index 000000000..34b8be003 --- /dev/null +++ b/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.styled.ts @@ -0,0 +1,141 @@ +import styled, { css } from 'styled-components'; +import { ComponentProps } from 'react'; + +export interface InputProps extends ComponentProps<'input'> { + inputSize?: 'S' | 'M' | 'L'; + searchIcon?: boolean; +} + +const INPUT_SIZES = { + S: 24, + M: 32, + L: 40, +}; + +interface OptionProps { + disabled?: boolean; + isHighlighted?: boolean; +} + +export const Wrapper = styled.div` + position: relative; + min-width: 200px; + &:hover { + svg:first-child { + fill: ${({ theme }) => theme.input.icon.hover}; + } + } + svg:first-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: 12px; + right: unset; + height: 16px; + width: 16px; + fill: ${({ theme }) => theme.input.icon.color}; + } + svg:last-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: unset; + right: 12px; + height: 16px; + width: 16px; + } +`; + +export const Input = styled.input( + ({ theme: { input }, inputSize, searchIcon }) => css` + background-color: ${input.backgroundColor.normal}; + border: 1px ${input.borderColor.normal} solid; + border-radius: 4px; + color: ${input.color.normal}; + height: ${inputSize ? `${INPUT_SIZES[inputSize]}px` : '32px'}; + width: 100%; + padding-left: ${searchIcon ? '36px' : '12px'}; + padding-right: 30px; + font-size: 14px; + box-sizing: border-box; + + &::placeholder { + color: ${input.color.placeholder.normal}; + font-size: 14px; + } + &:hover { + border-color: ${input.borderColor.hover}; + } + &:focus { + outline: none; + border-color: ${input.borderColor.focus}; + &::placeholder { + color: transparent; + } + } + &:read-only { + color: ${input.color.readOnly}; + border: none; + background-color: ${input.backgroundColor.readOnly}; + cursor: not-allowed; + } + ` +); + +export type StyledInputProps = ComponentProps; + +export const OptionList = styled.ul` + position: absolute; + top: 100%; + left: 0; + max-height: 228px; + margin-top: 4px; + background-color: ${({ theme }) => theme.select.backgroundColor.normal}; + border: 1px ${({ theme }) => theme.select.borderColor.normal} solid; + border-radius: 4px; + font-size: 14px; + line-height: 18px; + color: ${({ theme }) => theme.select.color.normal}; + overflow-y: auto; + z-index: 10; + max-width: 100%; + min-width: 100%; + display: flex; + flex-direction: column; + box-sizing: border-box; +`; + +export const Option = styled.li` + display: flex; + align-items: center; + list-style: none; + padding: 10px 12px; + transition: all 0.2s ease-in-out; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + gap: 5px; + color: ${({ theme, disabled }) => + theme.select.color[disabled ? 'disabled' : 'normal']}; + background-color: ${({ isHighlighted, theme }) => + isHighlighted ? theme.select.backgroundColor.hover : 'transparent'}; + + &:hover { + background-color: ${({ theme, disabled }) => + disabled ? 'transparent' : theme.select.backgroundColor.hover}; + } + + &:active { + background-color: ${({ theme }) => theme.select.backgroundColor.active}; + } +`; + +export const IconButtonWrapper = styled.span.attrs(() => ({ + role: 'button', + tabIndex: 0, +}))` + display: inline-block; + &:hover { + cursor: pointer; + } +`; diff --git a/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.tsx b/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.tsx new file mode 100644 index 000000000..4a8bfccee --- /dev/null +++ b/frontend/src/components/common/SearchAutocomplete/SearchAutocomplete.tsx @@ -0,0 +1,156 @@ +import React, { useRef, useEffect, useState } from 'react'; +import useClickOutside from 'lib/hooks/useClickOutside'; +import { SelectOption } from 'components/common/Select/Select'; +import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon'; +import SearchIcon from 'components/common/Icons/SearchIcon'; + +import * as S from './SearchAutocomplete.styled'; + +export interface SearchAutocompleteProps + extends Omit { + options: SelectOption[]; + value?: string; + onChange?: (option: string) => void; + inputSize?: 'S' | 'M' | 'L'; + minWidth?: string; + placeholder?: string; + searchIcon?: boolean; +} + +const SearchAutocomplete: React.FC = ({ + options = [], + value = '', + onChange, + inputSize = 'M', + placeholder = '', + minWidth, + searchIcon = true, + ...rest +}) => { + const [inputValue, setInputValue] = useState(value); + const [showOptions, setShowOptions] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(0); + + const selectContainerRef = useRef(null); + const optionListRef = useRef(null); + + const clickOutsideHandler = () => { + const isDisabledOption = (optionText: string) => + options.some((option) => option.value === optionText && option.disabled); + + if (!isDisabledOption(value) && showOptions) { + onChange?.(inputValue); + } + + setShowOptions(false); + setHighlightedIndex(0); + }; + useClickOutside(selectContainerRef, clickOutsideHandler); + + const filteredOptions = options.filter((option) => + option?.label?.toString().toLowerCase().includes(inputValue.toLowerCase()) + ); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + setShowOptions(true); + if (onChange) { + onChange(e.target.value); + } + }; + + const handleOptionClick = (option: SelectOption, index: number) => { + setInputValue(option?.value); + setShowOptions(false); + if (onChange) { + onChange(option.value); + } + setHighlightedIndex(index); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setHighlightedIndex((prev) => + Math.min(prev + 1, filteredOptions.length - 1) + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setHighlightedIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) { + handleOptionClick(filteredOptions[highlightedIndex], highlightedIndex); + } + } + }; + + useEffect(() => { + if (optionListRef.current && highlightedIndex >= 0) { + const option = optionListRef.current.children[ + highlightedIndex + ] as HTMLElement; + if (option) { + const optionRect = option.getBoundingClientRect(); + const listRect = optionListRef.current.getBoundingClientRect(); + if (optionRect.bottom > listRect.bottom) { + optionListRef.current.scrollTop += + optionRect.bottom - listRect.bottom; + } else if (optionRect.top < listRect.top) { + optionListRef.current.scrollTop -= listRect.top - optionRect.top; + } + } + } + }, [highlightedIndex]); + + const clearSearchValue = () => { + setInputValue(''); + setShowOptions(false); + setHighlightedIndex(0); + if (onChange) { + onChange(''); + } + }; + + return ( + + {searchIcon && } + setShowOptions(true)} + autoComplete="off" + placeholder={placeholder} + inputSize={inputSize} + onChange={handleInputChange} + onKeyDown={handleKeyDown} + /> + {showOptions && ( + + {filteredOptions.length > 0 ? ( + filteredOptions.map((option, index) => ( + handleOptionClick(option, index)} + > + {option.label} + + )) + ) : ( + No results + )} + + )} + + + + + + ); +}; + +export default SearchAutocomplete; From df90635682ed6396fd5c236d5f6749554ef80270 Mon Sep 17 00:00:00 2001 From: Alexis Demetriou Date: Mon, 9 Sep 2024 09:29:07 +0300 Subject: [PATCH 2/3] IPD-1331 - Implemented filtering for each field of the connectors grid --- frontend/src/components/common/MultiSearch/MultiSearch.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/common/MultiSearch/MultiSearch.tsx b/frontend/src/components/common/MultiSearch/MultiSearch.tsx index a07b8d900..99d7e1216 100644 --- a/frontend/src/components/common/MultiSearch/MultiSearch.tsx +++ b/frontend/src/components/common/MultiSearch/MultiSearch.tsx @@ -61,6 +61,7 @@ const MultiSearch: React.FC = ({ }; const clearAll = () => { + setInputValue(''); if (onChange) { onChange('', []); } From 03970343aee42aa5921795015f554bc6f7004d12 Mon Sep 17 00:00:00 2001 From: Alexis Demetriou Date: Mon, 16 Sep 2024 15:02:44 +0300 Subject: [PATCH 3/3] Fixed test issues --- .../src/components/Connect/List/__tests__/List.spec.tsx | 2 +- .../components/Connect/List/__tests__/ListPage.spec.tsx | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/components/Connect/List/__tests__/List.spec.tsx b/frontend/src/components/Connect/List/__tests__/List.spec.tsx index 82b4aab21..095b7a32d 100644 --- a/frontend/src/components/Connect/List/__tests__/List.spec.tsx +++ b/frontend/src/components/Connect/List/__tests__/List.spec.tsx @@ -56,7 +56,7 @@ describe('Connectors List', () => { it('renders', async () => { renderComponent(); expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByRole('row').length).toEqual(3); + expect(screen.getAllByRole('row').length).toEqual(4); }); it('opens broker when row clicked', async () => { diff --git a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx index 0cbe4dd78..932b083dc 100644 --- a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx +++ b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx @@ -67,13 +67,6 @@ describe('Connectors List Page', () => { }); }); - it('renders search input', async () => { - await renderComponent(); - expect( - screen.getByPlaceholderText('Search by Connect Name, Status or Type') - ).toBeInTheDocument(); - }); - it('renders list', async () => { await renderComponent(); expect(screen.getByText('Connectors List')).toBeInTheDocument();