#2325 Make connectors table rows clickable and #2076 Implement connec… (#2689)

* #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 <rzabaluev@provectus.com>
Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>
This commit is contained in:
Kris-K-Dev 2022-10-07 11:20:22 -04:00 committed by GitHub
parent 80eb2dccfe
commit 7d5b7de992
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 251 additions and 217 deletions

View file

@ -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

View file

@ -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<CellContext<FullConnectorInfo, unknown>> = ({
row,
}) => {
const { connect, name } = row.original;
const { clusterName } = useAppParams<ClusterNameRoute>();
const confirm = useConfirm();
const deleteMutation = useDeleteConnector({
clusterName,
connectName: connect,
connectorName: name,
});
const handleDelete = () => {
confirm(
<>
Are you sure want to remove <b>{name}</b> connector?
</>,
async () => {
await deleteMutation.mutateAsync();
}
);
};
return (
<Dropdown>
<DropdownItem onClick={handleDelete} danger>
Remove Connector
</DropdownItem>
</Dropdown>
);
};
export default ActionsCell;

View file

@ -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;
}
}
`;

View file

@ -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<ClusterNameRoute>();
const [searchParams] = useSearchParams();
const { data: connectors } = useConnectors(
@ -16,35 +20,30 @@ const List: React.FC = () => {
searchParams.get('q') || ''
);
const columns = React.useMemo<ColumnDef<FullConnectorInfo>[]>(
() => [
{ 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 (
<Table isFullwidth>
<thead>
<tr>
<TableHeaderCell title="Name" />
<TableHeaderCell title="Connect" />
<TableHeaderCell title="Type" />
<TableHeaderCell title="Plugin" />
<TableHeaderCell title="Topics" />
<TableHeaderCell title="Status" />
<TableHeaderCell title="Running Tasks" />
<TableHeaderCell> </TableHeaderCell>
</tr>
</thead>
<tbody>
{(!connectors || connectors.length) === 0 && (
<tr>
<td colSpan={10}>No connectors found</td>
</tr>
)}
{connectors?.map((connector) => (
<ListItem
key={connector.name}
connector={connector}
clusterName={clusterName}
<Table
data={connectors || []}
columns={columns}
enableSorting
onRowClick={({ original: { connect, name } }) =>
navigate(clusterConnectConnectorPath(clusterName, connect, name))
}
emptyMessage="No connectors found"
/>
))}
</tbody>
</Table>
);
};

View file

@ -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<ListItemProps> = ({
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 <b>{name}</b> connector?
</>,
async () => {
await deleteMutation.mutateAsync();
}
);
};
const runningTasks = React.useMemo(() => {
if (!tasksCount) return null;
return tasksCount - (failedTasksCount || 0);
}, [tasksCount, failedTasksCount]);
return (
<tr>
<TableKeyLink>
<NavLink to={clusterConnectConnectorPath(clusterName, connect, name)}>
{name}
</NavLink>
</TableKeyLink>
<td>{connect}</td>
<td>{type}</td>
<td>{connectorClass}</td>
<td>
<S.TagsWrapper>
{topics?.map((t) => (
<Tag key={t} color="gray">
<Link to={clusterTopicPath(clusterName, t)}>{t}</Link>
</Tag>
))}
</S.TagsWrapper>
</td>
<td>
{status && <Tag color={getTagColor(status.state)}>{status.state}</Tag>}
</td>
<td>
{runningTasks && (
<span>
{runningTasks} of {tasksCount}
</span>
)}
</td>
<td>
<div>
<Dropdown>
<DropdownItem onClick={handleDelete} danger>
Remove Connector
</DropdownItem>
</Dropdown>
</div>
</td>
</tr>
);
};
export default ListItem;

View file

@ -0,0 +1,21 @@
import React from 'react';
import { FullConnectorInfo } from 'generated-sources';
import { CellContext } from '@tanstack/react-table';
const RunningTasksCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
row,
}) => {
const { tasksCount, failedTasksCount } = row.original;
if (!tasksCount) {
return null;
}
return (
<>
{tasksCount - (failedTasksCount || 0)} of {tasksCount}
</>
);
};
export default RunningTasksCell;

View file

@ -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<CellContext<FullConnectorInfo, unknown>> = ({
row,
}) => {
const { topics } = row.original;
const { clusterName } = useAppParams<ClusterNameRoute>();
const navigate = useNavigate();
const navigateToTopic = (
e: React.KeyboardEvent | React.MouseEvent,
topic: string
) => {
e.preventDefault();
e.stopPropagation();
navigate(clusterTopicPath(clusterName, topic));
};
return (
<S.TagsWrapper>
{topics?.map((t) => (
<Tag key={t} color="gray">
<span
role="link"
onClick={(e) => navigateToTopic(e, t)}
onKeyDown={(e) => navigateToTopic(e, t)}
tabIndex={0}
>
{t}
</span>
</Tag>
))}
</S.TagsWrapper>
);
};
export default TopicsCell;

View file

@ -5,24 +5,28 @@ 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', () => () => (
<tr>
<td>List Item</td>
</tr>
));
jest.mock('lib/hooks/api/kafkaConnect', () => ({
useConnectors: jest.fn(),
useDeleteConnector: jest.fn(),
}));
const clusterName = 'local';
describe('Connectors List', () => {
const renderComponent = (contextValue: ContextProps = initialValue) =>
const renderComponent = (contextValue: ContextProps = initialValue) =>
render(
<ClusterContext.Provider value={contextValue}>
<WithRoute path={clusterConnectorsPath()}>
@ -32,22 +36,89 @@ describe('Connectors List', () => {
{ initialEntries: [clusterConnectorsPath(clusterName)] }
);
it('renders empty connectors Table', async () => {
(useConnectors as jest.Mock).mockImplementation(() => ({
data: [],
}));
await renderComponent();
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('No connectors found')).toBeInTheDocument();
});
it('renders connectors Table', async () => {
describe('Connectors List', () => {
describe('when the connectors are loaded', () => {
beforeEach(() => {
(useConnectors as jest.Mock).mockImplementation(() => ({
data: connectors,
}));
await renderComponent();
});
it('renders', async () => {
renderComponent();
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getAllByText('List Item').length).toEqual(2);
expect(screen.getAllByRole('row').length).toEqual(3);
});
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'
)
)
);
});
});
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();
});
});
});

View file

@ -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<ListItemProps> = {}) => (
<table>
<tbody>
<ListItem clusterName="local" connector={connector} {...props} />
</tbody>
</table>
);
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('');
});
});