Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion frontend/src/components/Topics/List/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
import TopicTable from 'components/Topics/List/TopicTable';
import { Action, ResourceType } from 'generated-sources';
import ResourcePageHeading from 'components/common/ResourcePageHeading/ResourcePageHeading';
import Fts from 'components/common/Fts/Fts';
import useFts from 'components/common/Fts/useFts';

const ListPage: React.FC = () => {
const { isReadOnly } = React.useContext(ClusterContext);
const [searchParams, setSearchParams] = useSearchParams();

useFts('topics');

// Set the search params to the url based on the localStorage value
React.useEffect(() => {
if (!searchParams.has('perPage')) {
Expand Down Expand Up @@ -62,7 +66,10 @@ const ListPage: React.FC = () => {
)}
</ResourcePageHeading>
<ControlPanelWrapper hasInput>
<Search placeholder="Search by Topic Name" />
<Search
placeholder="Search by Topic Name"
extraActions={<Fts resourceName="topics" />}
/>
<label>
<Switch
name="ShowInternalTopics"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/Topics/List/TopicTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ClusterContext from 'components/contexts/ClusterContext';
import { useTopics } from 'lib/hooks/api/topics';
import { PER_PAGE } from 'lib/constants';
import { useLocalStoragePersister } from 'components/common/NewTable/ColumnResizer/lib';
import useFts from 'components/common/Fts/useFts';

import { TopicTitleCell } from './TopicTitleCell';
import ActionsCell from './ActionsCell';
Expand All @@ -18,8 +19,11 @@ const TopicTable: React.FC = () => {
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
const [searchParams] = useSearchParams();
const { isReadOnly } = React.useContext(ClusterContext);
const { isFtsEnabled } = useFts('topics');

const { data } = useTopics({
clusterName,
fts: isFtsEnabled,
page: Number(searchParams.get('page') || 1),
perPage: Number(searchParams.get('perPage') || PER_PAGE),
search: searchParams.get('q') || undefined,
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/common/Fts/Fts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import FtsIcon from 'components/common/Icons/FtsIcon';
import styled from 'styled-components';

import useFts, { FtsAvailableResource } from './useFts';

export const IconWrapper = styled.span.attrs<{ active: boolean }>(() => ({
role: 'button',
tabIndex: 1,
}))`
display: inline-block;
&:hover {
cursor: pointer;
}
color: ${(props) =>
props.active
? props.theme.icons.ftsIcon.active
: props.theme.icons.ftsIcon.normal};
`;

const Fts = ({ resourceName }: { resourceName: FtsAvailableResource }) => {
const { handleSwitch, isFtsEnabled } = useFts(resourceName);

return (
<IconWrapper onClick={handleSwitch} active={isFtsEnabled}>
<FtsIcon />
</IconWrapper>
);
};

export default Fts;
37 changes: 37 additions & 0 deletions frontend/src/components/common/Fts/useFts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { useSearchParams } from 'react-router-dom';

export type FtsAvailableResource = 'topics';

const storageName = 'kafbat-ui_fts';

const useFts = (resourceName: FtsAvailableResource) => {
const [searchParams, setSearchParams] = useSearchParams();
const storageKey = `${storageName}:${resourceName}`;

React.useEffect(() => {
if (!!localStorage.getItem(storageKey) && !searchParams.has('fts')) {
searchParams.set('fts', 'true');
}
setSearchParams(searchParams);
}, []);

const handleSwitch = () => {
if (searchParams.has('fts')) {
localStorage.removeItem(storageKey);
searchParams.delete('fts');
} else {
localStorage.setItem(storageKey, 'true');
searchParams.set('fts', 'true');
}
searchParams.set('page', '1');
setSearchParams(searchParams);
};

return {
handleSwitch,
isFtsEnabled: !!searchParams.get('fts'),
};
};

export default useFts;
20 changes: 20 additions & 0 deletions frontend/src/components/common/Icons/FtsIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

const FtsIcon: React.FC = () => {
return (
<svg
fill="currentColor"
width="800px"
height="800px"
viewBox="0 0 56 56"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M 13.5039 50.9570 L 26.3476 50.9570 C 25.1055 49.9023 24.0508 48.6367 23.2773 47.1836 L 13.7148 47.1836 C 11.3008 47.1836 10.0117 45.9414 10.0117 43.5976 L 10.0117 8.1367 C 10.0117 5.8164 11.2773 4.4805 13.7148 4.4805 L 38.2070 4.4805 C 40.5508 4.4805 41.8867 5.7930 41.8867 8.1367 L 41.8867 28.5742 C 43.3398 29.3476 44.6055 30.3789 45.6602 31.6211 L 45.6602 8.0664 C 45.6602 3.1679 43.2461 .7070 38.3945 .7070 L 13.5039 .7070 C 8.6758 .7070 6.2383 3.1914 6.2383 8.0664 L 6.2383 43.6211 C 6.2383 48.5195 8.6758 50.9570 13.5039 50.9570 Z M 17.0898 14.0430 L 34.8555 14.0430 C 35.6758 14.0430 36.3086 13.3867 36.3086 12.5664 C 36.3086 11.7695 35.6758 11.1601 34.8555 11.1601 L 17.0898 11.1601 C 16.2227 11.1601 15.6133 11.7695 15.6133 12.5664 C 15.6133 13.3867 16.2227 14.0430 17.0898 14.0430 Z M 17.0898 22.2226 L 34.8555 22.2226 C 35.6758 22.2226 36.3086 21.5664 36.3086 20.7461 C 36.3086 19.9492 35.6758 19.3398 34.8555 19.3398 L 17.0898 19.3398 C 16.2227 19.3398 15.6133 19.9492 15.6133 20.7461 C 15.6133 21.5664 16.2227 22.2226 17.0898 22.2226 Z M 35.1367 50.9570 C 37.2461 50.9570 39.2383 50.3476 40.8789 49.2461 L 46.1524 54.5430 C 46.7148 55.0820 47.2305 55.2930 47.8633 55.2930 C 48.9414 55.2930 49.7617 54.4492 49.7617 53.2539 C 49.7617 52.7383 49.5040 52.2226 49.1056 51.8242 L 43.7617 46.4805 C 44.9570 44.7695 45.6602 42.6836 45.6602 40.4336 C 45.6602 34.5976 40.9492 29.8867 35.1367 29.8867 C 29.3242 29.8867 24.5664 34.6445 24.5664 40.4336 C 24.5664 46.2461 29.3242 50.9570 35.1367 50.9570 Z M 35.1367 47.6054 C 31.1524 47.6054 27.9180 44.3945 27.9180 40.4336 C 27.9180 36.5195 31.1524 33.2617 35.1367 33.2617 C 39.0508 33.2617 42.2851 36.5195 42.2851 40.4336 C 42.2851 44.3945 39.0742 47.6054 35.1367 47.6054 Z"
/>
</svg>
);
};

export default FtsIcon;
6 changes: 4 additions & 2 deletions frontend/src/components/common/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface InputProps
label?: React.ReactNode;
hint?: React.ReactNode;
clearIcon?: React.ReactNode;

actions?: React.ReactNode;
// Some may only accept integer, like `Number of Partitions`
// some may accept decimal
integerOnly?: boolean;
Expand Down Expand Up @@ -113,6 +113,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
label,
hint,
clearIcon,
actions,
...rest
} = props;

Expand Down Expand Up @@ -187,7 +188,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
ref={ref}
{...inputOptions}
/>
{clearIcon}
{search && clearIcon}
{actions}

{withError && isHookFormField && (
<S.FormError>
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/common/Search/Search.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import styled from 'styled-components';

export const Actions = styled.div`
position: absolute;
display: flex;
align-items: center;
top: 8px;
right: 8px;
gap: 8px;
svg:first-child {
position: initial;
height: 16px;
width: 16px;
fill: ${({ theme }) => theme.input.icon.color};
}
svg:last-child {
position: initial;
height: 16px;
width: 16px;
}
`;

export const IconButtonWrapper = styled.span.attrs(() => ({
role: 'button',
tabIndex: 0,
}))`
display: inline-block;
&:hover {
cursor: pointer;
}
`;
44 changes: 28 additions & 16 deletions frontend/src/components/common/Search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import React, { ComponentRef, useEffect, useRef } from 'react';
import React, {
ComponentRef,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { useDebouncedCallback } from 'use-debounce';
import Input from 'components/common/Input/Input';
import { useSearchParams } from 'react-router-dom';
import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
import styled from 'styled-components';

import * as S from './Search.styled';

interface SearchProps {
placeholder?: string;
disabled?: boolean;
onChange?: (value: string) => void;
value?: string;
extraActions?: ReactNode;
}

const IconButtonWrapper = styled.span.attrs(() => ({
role: 'button',
tabIndex: 0,
}))`
height: 16px !important;
display: inline-block;
&:hover {
cursor: pointer;
}
`;
const Search: React.FC<SearchProps> = ({
placeholder = 'Search',
disabled = false,
value,
onChange,
extraActions,
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const ref = useRef<ComponentRef<'input'>>(null);
const [showIcon, setShowIcon] = useState(!!value || !!searchParams.get('q'));

useEffect(() => {
if (ref.current !== null && value) {
Expand All @@ -38,6 +38,7 @@ const Search: React.FC<SearchProps> = ({
}, [value]);

const handleChange = useDebouncedCallback((e) => {
setShowIcon(!!e.target.value);
if (ref.current != null) {
ref.current.value = e.target.value;
}
Expand All @@ -59,12 +60,14 @@ const Search: React.FC<SearchProps> = ({
searchParams.set('q', '');
setSearchParams(searchParams);
}

if (ref.current != null) {
ref.current.value = '';
}
if (onChange) {
onChange('');
}
setShowIcon(false);
};

return (
Expand All @@ -77,10 +80,19 @@ const Search: React.FC<SearchProps> = ({
disabled={disabled}
ref={ref}
search
clearIcon={
<IconButtonWrapper onClick={clearSearchValue}>
<CloseCircleIcon />
</IconButtonWrapper>
actions={
<S.Actions>
{showIcon && (
<S.IconButtonWrapper
onClick={clearSearchValue}
data-testid="search-clear-button"
>
<CloseCircleIcon />
</S.IconButtonWrapper>
)}

{extraActions}
</S.Actions>
}
/>
);
Expand Down
25 changes: 18 additions & 7 deletions frontend/src/components/common/Search/__tests__/Search.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Search from 'components/common/Search/Search';
import React from 'react';
import { render } from 'lib/testHelpers';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import { screen, fireEvent } from '@testing-library/react';
import { useSearchParams } from 'react-router-dom';

jest.mock('use-debounce', () => ({
Expand Down Expand Up @@ -42,23 +42,34 @@ describe('Search', () => {
expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
});

it('Clear button is visible', () => {
it('Clear button is not visible by default', async () => {
render(<Search placeholder={placeholder} />);

const clearButton = screen.getByRole('button');
const clearButton = screen.queryByTestId('search-clear-button');
expect(clearButton).not.toBeInTheDocument();
});

it('Clear button is visible if value passed', async () => {
render(<Search placeholder={placeholder} value="text" />);

const clearButton = screen.queryByTestId('search-clear-button');
expect(clearButton).toBeInTheDocument();
});

it('Clear button should clear text from input', async () => {
render(<Search placeholder={placeholder} />);

const searchField = screen.getAllByRole('textbox')[0];
await userEvent.type(searchField, 'some text');
expect(searchField).toHaveValue('some text');
fireEvent.change(searchField, { target: { value: 'hello' } });
expect(searchField).toHaveValue('hello');

const clearButton = screen.getByRole('button');
await userEvent.click(clearButton);
let clearButton = screen.queryByTestId('search-clear-button');
expect(clearButton).toBeInTheDocument();
await userEvent.click(clearButton!);

expect(searchField).toHaveValue('');

clearButton = screen.queryByTestId('search-clear-button');
expect(clearButton).not.toBeInTheDocument();
});
});
8 changes: 8 additions & 0 deletions frontend/src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ const baseTheme = {
filterIcon: {
normal: Colors.brand[70],
},
ftsIcon: {
normal: Colors.neutral[30],
active: Colors.brand[70],
},
},
textArea: {
borderColor: {
Expand Down Expand Up @@ -1539,6 +1543,10 @@ export const darkTheme: ThemeType = {
normal: Colors.neutral[5],
},
menuIcon: Colors.brand[0],
ftsIcon: {
normal: Colors.neutral[50],
active: Colors.brand[10],
},
},
textArea: {
...baseTheme.textArea,
Expand Down
Loading