diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6d2cf845e1b39d5c550141385ce1521ed429da06 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ActionsCellTasks.tsx @@ -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> = ({ row }) => { + const { id } = row.original; + const routerProps = useAppParams(); + const restartMutation = useRestartConnectorTask(routerProps); + + const restartTaskHandler = (taskId?: number) => { + if (taskId === undefined) return; + restartMutation.mutateAsync(taskId); + }; + + return ( + + restartTaskHandler(id?.task)} + danger + confirm="Are you sure you want to restart the task?" + > + Restart task + + + ); +}; + +export default ActionsCellTasks; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx index 74dd89ab87339ebf7e33029709d49f44ba518e58..bb21e895380e20bd76ded20dd418d18353064a4a 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx @@ -1,69 +1,58 @@ import React from 'react'; -import { Table } from 'components/common/table/Table/Table.styled'; -import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { - useConnectorTasks, - useRestartConnectorTask, -} from 'lib/hooks/api/kafkaConnect'; +import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; import useAppParams from 'lib/hooks/useAppParams'; import { RouterParamsClusterConnectConnector } from 'lib/paths'; -import getTagColor from 'components/common/Tag/getTagColor'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import { Dropdown, DropdownItem } from 'components/common/Dropdown'; +import { ColumnDef, Row } from '@tanstack/react-table'; +import { Task } from 'generated-sources'; +import Table, { TagCell } from 'components/common/NewTable'; + +import ActionsCellTasks from './ActionsCellTasks'; + +const ExpandedTaskRow: React.FC<{ row: Row }> = ({ row }) => { + return
{row.original.status.trace}
; +}; + +const MAX_LENGTH = 100; const Tasks: React.FC = () => { const routerProps = useAppParams(); - const { data: tasks } = useConnectorTasks(routerProps); - const restartMutation = useRestartConnectorTask(routerProps); + const { data = [] } = useConnectorTasks(routerProps); - const restartTaskHandler = (taskId?: number) => { - if (taskId === undefined) return; - restartMutation.mutateAsync(taskId); - }; + const columns = React.useMemo[]>( + () => [ + { 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() || ''; + return trace.toString().length > MAX_LENGTH + ? `${trace.toString().substring(0, MAX_LENGTH - 3)}...` + : trace; + }, + meta: { width: '70%' }, + }, + { + id: 'actions', + header: '', + cell: ActionsCellTasks, + }, + ], + [] + ); return ( - - - - - - - - - - - - {tasks?.length === 0 && ( - - - - )} - {tasks?.map((task) => ( - - - - - - - - ))} - -
No tasks found
{task.status?.id}{task.status?.workerId} - - {task.status.state} - - {task.status.trace || 'null'} -
- - restartTaskHandler(task.id?.task)} - danger - > - Restart task - - -
-
+ row.original.status.trace?.length > 0} + renderSubComponent={ExpandedTaskRow} + /> ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx index efb72d1812c6dc13e54774b394af2e87c9e4014f..da38068a203119f1ae15e979956db9d0d46605b6 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx @@ -3,8 +3,13 @@ import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorTasksPath } from 'lib/paths'; import Tasks from 'components/Connect/Details/Tasks/Tasks'; import { tasks } from 'lib/fixtures/kafkaConnect'; -import { screen } from '@testing-library/dom'; -import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect'; +import { screen, within, waitFor } from '@testing-library/react'; +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', () => ({ useConnectorTasks: jest.fn(), @@ -13,30 +18,109 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({ const path = clusterConnectConnectorTasksPath('local', 'ghp', '1'); +const restartConnectorMock = jest.fn(); + 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( , { initialEntries: [path] } ); + }; it('renders empty table', () => { - (useConnectorTasks as jest.Mock).mockImplementation(() => ({ - data: [], - })); - renderComponent(); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByText('No tasks found')).toBeInTheDocument(); }); it('renders tasks table', () => { - (useConnectorTasks as jest.Mock).mockImplementation(() => ({ - data: tasks, - })); - renderComponent(); + renderComponent(tasks); 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()); + }); }); }); diff --git a/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx index d53c3d79028a5b3c6f476499d6dd86d9ca091485..ff5e501d527af69237d74b564b6711e82ac66093 100644 --- a/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx +++ b/kafka-ui-react-app/src/components/common/NewTable/ExpanderCell.tsx @@ -12,6 +12,7 @@ const ExpanderCell: React.FC> = ({ row }) => ( xmlns="http://www.w3.org/2000/svg" role="button" aria-label="Expand row" + $disabled={!row.getCanExpand()} > {row.getIsExpanded() ? ( ` - & > path { - fill: ${table.expander.normal}; - &:hover { - fill: ${table.expander.hover}; +export const ExpaderButton = styled.svg<{ $disabled: boolean }>( + ({ theme: { table }, $disabled }) => css` + & > path { + fill: ${table.expander[$disabled ? 'disabled' : 'normal']}; } - } -` + + &:hover > path { + fill: ${table.expander[$disabled ? 'disabled' : 'hover']}; + } + ` ); interface ThProps { diff --git a/kafka-ui-react-app/src/components/common/NewTable/Table.tsx b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx index aaf02ea40824557bee061f41188280259dd2b563..3b3a780674cb63bfb5917c3e3031dfe6a355913c 100644 --- a/kafka-ui-react-app/src/components/common/NewTable/Table.tsx +++ b/kafka-ui-react-app/src/components/common/NewTable/Table.tsx @@ -246,15 +246,15 @@ const Table: React.FC> = ({ } > {!!enableRowSelection && ( - )} - {row.getCanExpand() && ( - + ))} {row.getIsExpanded() && renderSubComponent && ( diff --git a/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts b/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts index f885ea405df01697d3907c5a4dbb09157da28908..8a79760e661a0357adf228dd629ff44bb0d7a144 100644 --- a/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts +++ b/kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts @@ -93,6 +93,8 @@ export const tasks: Task[] = [ id: 3, state: ConnectorTaskStatus.RUNNING, 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: { 'batch.size': '3000', diff --git a/kafka-ui-react-app/src/theme/theme.ts b/kafka-ui-react-app/src/theme/theme.ts index 4c057645ef1c80af189a609588ba284c9de4f712..a962b2a5356eae02a1fb391db391b6ce20990d9f 100644 --- a/kafka-ui-react-app/src/theme/theme.ts +++ b/kafka-ui-react-app/src/theme/theme.ts @@ -346,6 +346,7 @@ const theme = { expander: { normal: Colors.brand[50], hover: Colors.brand[20], + disabled: Colors.neutral[10], }, }, primaryTab: {
+ {flexRender( SelectRowCell, row.getVisibleCells()[0].getContext() )} + {table.getCanSomeRowsExpand() && ( + {flexRender( ExpanderCell, row.getVisibleCells()[0].getContext() @@ -264,7 +264,9 @@ const Table: React.FC> = ({ {row .getVisibleCells() .map(({ id, getContext, column: { columnDef } }) => ( - {flexRender(columnDef.cell, getContext())} + {flexRender(columnDef.cell, getContext())} +