Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions frontend/src/components/Connect/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,11 +18,7 @@ import RunningTasksCell from './RunningTasksCell';
const List: React.FC = () => {
const navigate = useNavigate();
const { clusterName } = useAppParams<ClusterNameRoute>();
const [searchParams] = useSearchParams();
const { data: connectors } = useConnectors(
clusterName,
searchParams.get('q') || ''
);
const { data: connectors } = useConnectors(clusterName);

const columns = React.useMemo<ColumnDef<FullConnectorInfo>[]>(
() => [
Expand All @@ -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 (
<Table
data={connectors || []}
Expand All @@ -44,6 +101,8 @@ const List: React.FC = () => {
}
emptyMessage="No connectors found"
setRowId={(originalRow) => `${originalRow.name}-${originalRow.connect}`}
enableColumnSearch
columnSearchPlaceholders={columnSearchPlaceholders}
/>
);
};
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/components/Connect/List/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,9 +79,6 @@ const ListPage: React.FC = () => {
</Metrics.Indicator>
</Metrics.Section>
</Metrics.Wrapper>
<ControlPanelWrapper hasInput>
<Search placeholder="Search by Connect Name, Status or Type" />
</ControlPanelWrapper>
<Suspense fallback={<PageLoader />}>
<List />
</Suspense>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/common/Input/Input.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -52,6 +53,7 @@ export const Input = styled.input<InputProps>(
: '40px'};
width: 100%;
padding-left: ${search ? '36px' : '12px'};
padding-right: 30px;
font-size: 14px;

&::placeholder {
Expand Down
130 changes: 130 additions & 0 deletions frontend/src/components/common/MultiSearch/MultiSearch.styled.ts
Original file line number Diff line number Diff line change
@@ -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<InputProps>(
({ 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<InputProps>(
({ 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<InputProps>(
({ 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;
`;
116 changes: 116 additions & 0 deletions frontend/src/components/common/MultiSearch/MultiSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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<S.InputProps, 'onChange'> {
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<MultiSearchProps> = ({
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 = () => {
setInputValue('');
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 (
<S.Wrapper ref={selectContainerRef}>
{searchIcon && <SearchIcon />}
<S.ValuesContainer>
{visibleTags.map((tagValue) => (
<S.Tag key={tagValue}>
{tagValue}
<S.RemoveButton onClick={() => handleRemove(tagValue)}>
<CloseCircleIcon />
</S.RemoveButton>
</S.Tag>
))}
{!showAllTags && remainingTagsCount > 0 && (
<S.RemainingTagCount>+{remainingTagsCount}</S.RemainingTagCount>
)}
<S.Input
{...rest}
value={inputValue}
values={values}
placeholder={values.length <= MAX_VISIBLE_TAGS ? placeholder : name}
inputSize={inputSize}
onChange={handleInputChange}
onKeyDown={handleKeyEnter}
onFocus={handleFocus}
isFocused={showAllTags}
/>
</S.ValuesContainer>
<S.IconButtonWrapper onClick={clearAll}>
<CloseCircleIcon />
</S.IconButtonWrapper>
</S.Wrapper>
);
};

export default MultiSearch;
Loading
Loading