From 7d5b7de992da9ddb1a018a6a61db66c4dcb204aa Mon Sep 17 00:00:00 2001 From: Kris-K-Dev <92114648+Kris-K-Dev@users.noreply.github.com> Date: Fri, 7 Oct 2022 11:20:22 -0400 Subject: [PATCH] =?UTF-8?q?#2325=20Make=20connectors=20table=20rows=20clic?= =?UTF-8?q?kable=20and=20#2076=20Implement=20connec=E2=80=A6=20(#2689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #2325 Make connectors table rows clickable and #2076 Implement connectors sorting * #2325 fix status sorting * fix ConnectorsTests * #2325 code review * #2325 add test coverage * #2325 code review fix * #2325 fix redirects for topics Co-authored-by: Roman Zabaluev Co-authored-by: VladSenyuta --- .../ui/pages/connector/KafkaConnectList.java | 4 +- .../components/Connect/List/ActionsCell.tsx | 43 ++++++ .../components/Connect/List/List.styled.ts | 6 + .../src/components/Connect/List/List.tsx | 65 +++++---- .../src/components/Connect/List/ListItem.tsx | 98 ------------- .../Connect/List/RunningTasksCell.tsx | 21 +++ .../components/Connect/List/TopicsCell.tsx | 45 ++++++ .../Connect/List/__tests__/List.spec.tsx | 133 ++++++++++++++---- .../Connect/List/__tests__/ListItem.spec.tsx | 53 ------- 9 files changed, 251 insertions(+), 217 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx delete mode 100644 kafka-ui-react-app/src/components/Connect/List/ListItem.tsx create mode 100644 kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx create mode 100644 kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx delete mode 100644 kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java index b5986c7919..89a73ff693 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/connector/KafkaConnectList.java @@ -28,14 +28,14 @@ public class KafkaConnectList { @Step public KafkaConnectList openConnector(String connectorName) { - $(By.linkText(connectorName)).click(); + $x("//tbody//td[1][text()='" + connectorName + "']").shouldBe(Condition.enabled).click(); return this; } @Step public boolean isConnectorVisible(String connectorName) { $(By.xpath("//table")).shouldBe(Condition.visible); - return isVisible($x("//tbody//td[1]//a[text()='" + connectorName + "']")); + return isVisible($x("//tbody//td[1][text()='" + connectorName + "']")); } @Step diff --git a/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx new file mode 100644 index 0000000000..30b3df8a56 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { FullConnectorInfo } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { ClusterNameRoute } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; +import { Dropdown, DropdownItem } from 'components/common/Dropdown'; +import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect'; +import { useConfirm } from 'lib/hooks/useConfirm'; + +const ActionsCell: React.FC> = ({ + row, +}) => { + const { connect, name } = row.original; + + const { clusterName } = useAppParams(); + + const confirm = useConfirm(); + const deleteMutation = useDeleteConnector({ + clusterName, + connectName: connect, + connectorName: name, + }); + + const handleDelete = () => { + confirm( + <> + Are you sure want to remove {name} connector? + , + async () => { + await deleteMutation.mutateAsync(); + } + ); + }; + return ( + + + Remove Connector + + + ); +}; + +export default ActionsCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/List.styled.ts b/kafka-ui-react-app/src/components/Connect/List/List.styled.ts index f0e2631d2c..799915fcb1 100644 --- a/kafka-ui-react-app/src/components/Connect/List/List.styled.ts +++ b/kafka-ui-react-app/src/components/Connect/List/List.styled.ts @@ -3,4 +3,10 @@ import styled from 'styled-components'; export const TagsWrapper = styled.div` display: flex; flex-wrap: wrap; + span { + color: rgb(76, 76, 255) !important; + &:hover { + color: rgb(23, 23, 207) !important; + } + } `; diff --git a/kafka-ui-react-app/src/components/Connect/List/List.tsx b/kafka-ui-react-app/src/components/Connect/List/List.tsx index 75a782fa0a..b5935e7bab 100644 --- a/kafka-ui-react-app/src/components/Connect/List/List.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/List.tsx @@ -1,14 +1,18 @@ import React from 'react'; import useAppParams from 'lib/hooks/useAppParams'; -import { ClusterNameRoute } from 'lib/paths'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; +import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths'; +import Table, { TagCell } from 'components/common/NewTable'; +import { FullConnectorInfo } from 'generated-sources'; import { useConnectors } from 'lib/hooks/api/kafkaConnect'; -import { useSearchParams } from 'react-router-dom'; +import { ColumnDef } from '@tanstack/react-table'; +import { useNavigate, useSearchParams } from 'react-router-dom'; -import ListItem from './ListItem'; +import ActionsCell from './ActionsCell'; +import TopicsCell from './TopicsCell'; +import RunningTasksCell from './RunningTasksCell'; const List: React.FC = () => { + const navigate = useNavigate(); const { clusterName } = useAppParams(); const [searchParams] = useSearchParams(); const { data: connectors } = useConnectors( @@ -16,35 +20,30 @@ const List: React.FC = () => { searchParams.get('q') || '' ); + const columns = React.useMemo[]>( + () => [ + { header: 'Name', accessorKey: 'name' }, + { header: 'Connect', accessorKey: 'connect' }, + { header: 'Type', accessorKey: 'type' }, + { header: 'Plugin', accessorKey: 'connectorClass' }, + { header: 'Topics', cell: TopicsCell }, + { header: 'Status', accessorKey: 'status.state', cell: TagCell }, + { header: 'Running Tasks', cell: RunningTasksCell }, + { header: '', id: 'action', cell: ActionsCell }, + ], + [] + ); + return ( - - - - - - - - - - - - - - - {(!connectors || connectors.length) === 0 && ( - - - - )} - {connectors?.map((connector) => ( - - ))} - -
No connectors found
+ + navigate(clusterConnectConnectorPath(clusterName, connect, name)) + } + emptyMessage="No connectors found" + /> ); }; diff --git a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx b/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx deleted file mode 100644 index 4d10001947..0000000000 --- a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import { FullConnectorInfo } from 'generated-sources'; -import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths'; -import { ClusterName } from 'redux/interfaces'; -import { Link, NavLink } from 'react-router-dom'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; -import getTagColor from 'components/common/Tag/getTagColor'; -import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect'; -import { Dropdown, DropdownItem } from 'components/common/Dropdown'; -import { useConfirm } from 'lib/hooks/useConfirm'; - -import * as S from './List.styled'; - -export interface ListItemProps { - clusterName: ClusterName; - connector: FullConnectorInfo; -} - -const ListItem: React.FC = ({ - clusterName, - connector: { - name, - connect, - type, - connectorClass, - topics, - status, - tasksCount, - failedTasksCount, - }, -}) => { - const confirm = useConfirm(); - const deleteMutation = useDeleteConnector({ - clusterName, - connectName: connect, - connectorName: name, - }); - - const handleDelete = () => { - confirm( - <> - Are you sure want to remove {name} connector? - , - async () => { - await deleteMutation.mutateAsync(); - } - ); - }; - - const runningTasks = React.useMemo(() => { - if (!tasksCount) return null; - return tasksCount - (failedTasksCount || 0); - }, [tasksCount, failedTasksCount]); - - return ( - - - - {name} - - - - - - - - - - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx b/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx new file mode 100644 index 0000000000..4c3293d44c --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/RunningTasksCell.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { FullConnectorInfo } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; + +const RunningTasksCell: React.FC> = ({ + row, +}) => { + const { tasksCount, failedTasksCount } = row.original; + + if (!tasksCount) { + return null; + } + + return ( + <> + {tasksCount - (failedTasksCount || 0)} of {tasksCount} + + ); +}; + +export default RunningTasksCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx b/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx new file mode 100644 index 0000000000..ee48c2d2d3 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/TopicsCell.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { FullConnectorInfo } from 'generated-sources'; +import { CellContext } from '@tanstack/react-table'; +import { useNavigate } from 'react-router-dom'; +import { Tag } from 'components/common/Tag/Tag.styled'; +import { ClusterNameRoute, clusterTopicPath } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; + +import * as S from './List.styled'; + +const TopicsCell: React.FC> = ({ + row, +}) => { + const { topics } = row.original; + const { clusterName } = useAppParams(); + const navigate = useNavigate(); + + const navigateToTopic = ( + e: React.KeyboardEvent | React.MouseEvent, + topic: string + ) => { + e.preventDefault(); + e.stopPropagation(); + navigate(clusterTopicPath(clusterName, topic)); + }; + + return ( + + {topics?.map((t) => ( + + navigateToTopic(e, t)} + onKeyDown={(e) => navigateToTopic(e, t)} + tabIndex={0} + > + {t} + + + ))} + + ); +}; + +export default TopicsCell; diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx index a76b8092f2..04a7ba8150 100644 --- a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx @@ -5,49 +5,120 @@ import ClusterContext, { initialValue, } from 'components/contexts/ClusterContext'; import List from 'components/Connect/List/List'; -import { screen } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { render, WithRoute } from 'lib/testHelpers'; -import { clusterConnectorsPath } from 'lib/paths'; -import { useConnectors } from 'lib/hooks/api/kafkaConnect'; +import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; +import { useConnectors, useDeleteConnector } from 'lib/hooks/api/kafkaConnect'; + +const mockedUsedNavigate = jest.fn(); +const mockDelete = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedUsedNavigate, +})); -jest.mock('components/Connect/List/ListItem', () => () => ( - - - -)); jest.mock('lib/hooks/api/kafkaConnect', () => ({ useConnectors: jest.fn(), + useDeleteConnector: jest.fn(), })); const clusterName = 'local'; +const renderComponent = (contextValue: ContextProps = initialValue) => + render( + + + + + , + { initialEntries: [clusterConnectorsPath(clusterName)] } + ); + describe('Connectors List', () => { - const renderComponent = (contextValue: ContextProps = initialValue) => - render( - - - - - , - { initialEntries: [clusterConnectorsPath(clusterName)] } - ); + describe('when the connectors are loaded', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: connectors, + })); + }); - it('renders empty connectors Table', async () => { - (useConnectors as jest.Mock).mockImplementation(() => ({ - data: [], - })); + it('renders', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('row').length).toEqual(3); + }); - await renderComponent(); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getByText('No connectors found')).toBeInTheDocument(); + it('opens broker when row clicked', async () => { + renderComponent(); + await act(() => { + userEvent.click( + screen.getByRole('row', { + name: 'hdfs-source-connector first SOURCE FileStreamSource a b c RUNNING 2 of 2', + }) + ); + }); + await waitFor(() => + expect(mockedUsedNavigate).toBeCalledWith( + clusterConnectConnectorPath( + clusterName, + 'first', + 'hdfs-source-connector' + ) + ) + ); + }); }); - it('renders connectors Table', async () => { - (useConnectors as jest.Mock).mockImplementation(() => ({ - data: connectors, - })); - await renderComponent(); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByText('List Item').length).toEqual(2); + describe('when table is empty', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: [], + })); + }); + + it('renders empty table', async () => { + renderComponent(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect( + screen.getByRole('row', { name: 'No connectors found' }) + ).toBeInTheDocument(); + }); + }); + + describe('when remove connector modal is open', () => { + beforeEach(() => { + (useConnectors as jest.Mock).mockImplementation(() => ({ + data: connectors, + })); + (useDeleteConnector as jest.Mock).mockImplementation(() => ({ + mutateAsync: mockDelete, + })); + }); + + it('calls removeConnector on confirm', async () => { + renderComponent(); + const removeButton = screen.getAllByText('Remove Connector')[0]; + await waitFor(() => userEvent.click(removeButton)); + + const submitButton = screen.getAllByRole('button', { + name: 'Confirm', + })[0]; + await act(() => userEvent.click(submitButton)); + expect(mockDelete).toHaveBeenCalledWith(); + }); + + it('closes the modal when cancel button is clicked', async () => { + renderComponent(); + const removeButton = screen.getAllByText('Remove Connector')[0]; + await waitFor(() => userEvent.click(removeButton)); + + const cancelButton = screen.getAllByRole('button', { + name: 'Cancel', + })[0]; + await waitFor(() => userEvent.click(cancelButton)); + expect(cancelButton).not.toBeInTheDocument(); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx deleted file mode 100644 index 715d0aeb4d..0000000000 --- a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { connectors } from 'lib/fixtures/kafkaConnect'; -import ListItem, { ListItemProps } from 'components/Connect/List/ListItem'; -import { screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; - -describe('Connectors ListItem', () => { - const connector = connectors[0]; - const setupWrapper = (props: Partial = {}) => ( -
{connect}{type}{connectorClass} - - {topics?.map((t) => ( - - {t} - - ))} - - - {status && {status.state}} - - {runningTasks && ( - - {runningTasks} of {tasksCount} - - )} - -
- - - Remove Connector - - -
-
List Item
- - - -
- ); - - it('renders item', () => { - render(setupWrapper()); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2'); - }); - - it('topics tags are sorted', () => { - render(setupWrapper()); - const getLink = screen.getAllByRole('link'); - expect(getLink[1]).toHaveTextContent('a'); - expect(getLink[2]).toHaveTextContent('b'); - expect(getLink[3]).toHaveTextContent('c'); - }); - - it('renders item with failed tasks', () => { - render( - setupWrapper({ - connector: { - ...connector, - failedTasksCount: 1, - }, - }) - ); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent('1 of 2'); - }); - - it('does not render info about tasks if taksCount is undefined', () => { - render( - setupWrapper({ - connector: { - ...connector, - tasksCount: undefined, - }, - }) - ); - expect(screen.getAllByRole('cell')[6]).toHaveTextContent(''); - }); -});