Bugfix/2320 move tasks to a new table (#2455)

* #2320 #2321 move tasks to a new table

* #2320 #2321 test coverage

* #2320 #2321 code review fix

* Fix Expandable rows

Co-authored-by: Oleg Shuralev <workshur@gmail.com>
This commit is contained in:
KriKiparoidze 2022-08-18 05:28:51 -05:00 committed by GitHub
parent a5f539c62a
commit 95a0306143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 81 deletions

View file

@ -0,0 +1,32 @@
import React from 'react';
import { Task } from 'generated-sources';
import { CellContext } from '@tanstack/react-table';
import useAppParams from 'lib/hooks/useAppParams';
import { useRestartConnectorTask } from 'lib/hooks/api/kafkaConnect';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
const ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {
const { id } = row.original;
const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
const restartMutation = useRestartConnectorTask(routerProps);
const restartTaskHandler = (taskId?: number) => {
if (taskId === undefined) return;
restartMutation.mutateAsync(taskId);
};
return (
<Dropdown>
<DropdownItem
onClick={() => restartTaskHandler(id?.task)}
danger
confirm="Are you sure you want to restart the task?"
>
<span>Restart task</span>
</DropdownItem>
</Dropdown>
);
};
export default ActionsCellTasks;

View file

@ -1,69 +1,58 @@
import React from 'react'; import React from 'react';
import { Table } from 'components/common/table/Table/Table.styled'; import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import {
useConnectorTasks,
useRestartConnectorTask,
} from 'lib/hooks/api/kafkaConnect';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import { RouterParamsClusterConnectConnector } from 'lib/paths'; import { RouterParamsClusterConnectConnector } from 'lib/paths';
import getTagColor from 'components/common/Tag/getTagColor'; import { ColumnDef, Row } from '@tanstack/react-table';
import { Tag } from 'components/common/Tag/Tag.styled'; import { Task } from 'generated-sources';
import { Dropdown, DropdownItem } from 'components/common/Dropdown'; import Table, { TagCell } from 'components/common/NewTable';
import ActionsCellTasks from './ActionsCellTasks';
const ExpandedTaskRow: React.FC<{ row: Row<Task> }> = ({ row }) => {
return <div>{row.original.status.trace}</div>;
};
const MAX_LENGTH = 100;
const Tasks: React.FC = () => { const Tasks: React.FC = () => {
const routerProps = useAppParams<RouterParamsClusterConnectConnector>(); const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
const { data: tasks } = useConnectorTasks(routerProps); const { data = [] } = useConnectorTasks(routerProps);
const restartMutation = useRestartConnectorTask(routerProps);
const restartTaskHandler = (taskId?: number) => { const columns = React.useMemo<ColumnDef<Task>[]>(
if (taskId === undefined) return; () => [
restartMutation.mutateAsync(taskId); { header: 'ID', accessorKey: 'status.id' },
}; { header: 'Worker', accessorKey: 'status.workerId' },
{ header: 'State', accessorKey: 'status.state', cell: TagCell },
{
header: 'Trace',
accessorKey: 'status.trace',
enableSorting: false,
cell: ({ getValue }) => {
const trace = getValue<string>() || '';
return trace.toString().length > MAX_LENGTH
? `${trace.toString().substring(0, MAX_LENGTH - 3)}...`
: trace;
},
meta: { width: '70%' },
},
{
id: 'actions',
header: '',
cell: ActionsCellTasks,
},
],
[]
);
return ( return (
<Table isFullwidth> <Table
<thead> columns={columns}
<tr> data={data}
<TableHeaderCell title="ID" /> emptyMessage="No tasks found"
<TableHeaderCell title="Worker" /> enableSorting
<TableHeaderCell title="State" /> getRowCanExpand={(row) => row.original.status.trace?.length > 0}
<TableHeaderCell title="Trace" /> renderSubComponent={ExpandedTaskRow}
<TableHeaderCell /> />
</tr>
</thead>
<tbody>
{tasks?.length === 0 && (
<tr>
<td colSpan={10}>No tasks found</td>
</tr>
)}
{tasks?.map((task) => (
<tr key={task.status?.id}>
<td>{task.status?.id}</td>
<td>{task.status?.workerId}</td>
<td>
<Tag color={getTagColor(task.status.state)}>
{task.status.state}
</Tag>
</td>
<td>{task.status.trace || 'null'}</td>
<td style={{ width: '5%' }}>
<div>
<Dropdown>
<DropdownItem
onClick={() => restartTaskHandler(task.id?.task)}
danger
>
<span>Restart task</span>
</DropdownItem>
</Dropdown>
</div>
</td>
</tr>
))}
</tbody>
</Table>
); );
}; };

View file

@ -3,8 +3,13 @@ import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorTasksPath } from 'lib/paths'; import { clusterConnectConnectorTasksPath } from 'lib/paths';
import Tasks from 'components/Connect/Details/Tasks/Tasks'; import Tasks from 'components/Connect/Details/Tasks/Tasks';
import { tasks } from 'lib/fixtures/kafkaConnect'; import { tasks } from 'lib/fixtures/kafkaConnect';
import { screen } from '@testing-library/dom'; import { screen, within, waitFor } from '@testing-library/react';
import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; import userEvent from '@testing-library/user-event';
import {
useConnectorTasks,
useRestartConnectorTask,
} from 'lib/hooks/api/kafkaConnect';
import { Task } from 'generated-sources';
jest.mock('lib/hooks/api/kafkaConnect', () => ({ jest.mock('lib/hooks/api/kafkaConnect', () => ({
useConnectorTasks: jest.fn(), useConnectorTasks: jest.fn(),
@ -13,30 +18,109 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({
const path = clusterConnectConnectorTasksPath('local', 'ghp', '1'); const path = clusterConnectConnectorTasksPath('local', 'ghp', '1');
const restartConnectorMock = jest.fn();
describe('Tasks', () => { describe('Tasks', () => {
const renderComponent = () => beforeEach(() => {
(useRestartConnectorTask as jest.Mock).mockImplementation(() => ({
mutateAsync: restartConnectorMock,
}));
});
const renderComponent = (currentData: Task[] | undefined = undefined) => {
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
data: currentData,
}));
render( render(
<WithRoute path={clusterConnectConnectorTasksPath()}> <WithRoute path={clusterConnectConnectorTasksPath()}>
<Tasks /> <Tasks />
</WithRoute>, </WithRoute>,
{ initialEntries: [path] } { initialEntries: [path] }
); );
};
it('renders empty table', () => { it('renders empty table', () => {
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
data: [],
}));
renderComponent(); renderComponent();
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('No tasks found')).toBeInTheDocument(); expect(screen.getByText('No tasks found')).toBeInTheDocument();
}); });
it('renders tasks table', () => { it('renders tasks table', () => {
(useConnectorTasks as jest.Mock).mockImplementation(() => ({ renderComponent(tasks);
data: tasks,
}));
renderComponent();
expect(screen.getAllByRole('row').length).toEqual(tasks.length + 1); expect(screen.getAllByRole('row').length).toEqual(tasks.length + 1);
expect(
screen.getByRole('row', {
name: '1 kafka-connect0:8083 RUNNING',
})
).toBeInTheDocument();
});
it('renders truncates long trace and expands', () => {
renderComponent(tasks);
const trace = tasks[2]?.status?.trace || '';
const truncatedTrace = trace.toString().substring(0, 100 - 3);
const thirdRow = screen.getByRole('row', {
name: `3 kafka-connect0:8083 RUNNING ${truncatedTrace}...`,
});
expect(thirdRow).toBeInTheDocument();
const expandedDetails = screen.queryByText(trace);
// Full trace is not visible
expect(expandedDetails).not.toBeInTheDocument();
userEvent.click(thirdRow);
expect(
screen.getByRole('row', {
name: trace,
})
).toBeInTheDocument();
});
describe('Action button', () => {
const expectDropdownExists = () => {
const firstTaskRow = screen.getByRole('row', {
name: '1 kafka-connect0:8083 RUNNING',
});
expect(firstTaskRow).toBeInTheDocument();
const extBtn = within(firstTaskRow).getByRole('button', {
name: 'Dropdown Toggle',
});
expect(extBtn).toBeEnabled();
userEvent.click(extBtn);
expect(screen.getByRole('menu')).toBeInTheDocument();
};
it('renders action button', () => {
renderComponent(tasks);
expectDropdownExists();
expect(
screen.getAllByRole('button', { name: 'Dropdown Toggle' }).length
).toEqual(tasks.length);
// Action buttons are enabled
const actionBtn = screen.getAllByRole('menuitem');
expect(actionBtn[0]).toHaveTextContent('Restart task');
});
it('works as expected', async () => {
renderComponent(tasks);
expectDropdownExists();
const actionBtn = screen.getAllByRole('menuitem');
expect(actionBtn[0]).toHaveTextContent('Restart task');
userEvent.click(actionBtn[0]);
expect(
screen.getByText('Are you sure you want to restart the task?')
).toBeInTheDocument();
expect(screen.getByText('Confirm the action')).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
await waitFor(() => expect(restartConnectorMock).toHaveBeenCalled());
});
}); });
}); });

View file

@ -12,6 +12,7 @@ const ExpanderCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
role="button" role="button"
aria-label="Expand row" aria-label="Expand row"
$disabled={!row.getCanExpand()}
> >
{row.getIsExpanded() ? ( {row.getIsExpanded() ? (
<path <path

View file

@ -1,14 +1,15 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
export const ExpaderButton = styled.svg( export const ExpaderButton = styled.svg<{ $disabled: boolean }>(
({ theme: { table } }) => ` ({ theme: { table }, $disabled }) => css`
& > path { & > path {
fill: ${table.expander.normal}; fill: ${table.expander[$disabled ? 'disabled' : 'normal']};
&:hover {
fill: ${table.expander.hover};
} }
}
` &:hover > path {
fill: ${table.expander[$disabled ? 'disabled' : 'hover']};
}
`
); );
interface ThProps { interface ThProps {

View file

@ -246,15 +246,15 @@ const Table: React.FC<TableProps<any>> = ({
} }
> >
{!!enableRowSelection && ( {!!enableRowSelection && (
<td key={`${row.id}-select`}> <td key={`${row.id}-select`} style={{ width: '1px' }}>
{flexRender( {flexRender(
SelectRowCell, SelectRowCell,
row.getVisibleCells()[0].getContext() row.getVisibleCells()[0].getContext()
)} )}
</td> </td>
)} )}
{row.getCanExpand() && ( {table.getCanSomeRowsExpand() && (
<td key={`${row.id}-expander`}> <td key={`${row.id}-expander`} style={{ width: '1px' }}>
{flexRender( {flexRender(
ExpanderCell, ExpanderCell,
row.getVisibleCells()[0].getContext() row.getVisibleCells()[0].getContext()
@ -264,7 +264,9 @@ const Table: React.FC<TableProps<any>> = ({
{row {row
.getVisibleCells() .getVisibleCells()
.map(({ id, getContext, column: { columnDef } }) => ( .map(({ id, getContext, column: { columnDef } }) => (
<td key={id}>{flexRender(columnDef.cell, getContext())}</td> <td key={id} style={columnDef.meta}>
{flexRender(columnDef.cell, getContext())}
</td>
))} ))}
</S.Row> </S.Row>
{row.getIsExpanded() && renderSubComponent && ( {row.getIsExpanded() && renderSubComponent && (

View file

@ -93,6 +93,8 @@ export const tasks: Task[] = [
id: 3, id: 3,
state: ConnectorTaskStatus.RUNNING, state: ConnectorTaskStatus.RUNNING,
workerId: 'kafka-connect0:8083', workerId: 'kafka-connect0:8083',
trace:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
}, },
config: { config: {
'batch.size': '3000', 'batch.size': '3000',

View file

@ -346,6 +346,7 @@ const theme = {
expander: { expander: {
normal: Colors.brand[50], normal: Colors.brand[50],
hover: Colors.brand[20], hover: Colors.brand[20],
disabled: Colors.neutral[10],
}, },
}, },
primaryTab: { primaryTab: {