Compare commits

...
Sign in to create a new pull request.

11 commits

Author SHA1 Message Date
Kamila Alekbaeva
34921e0c31 Merge branch 'master' into bulk-connectors-ops 2023-04-26 11:24:17 -04:00
Kamila Alekbaeva
42eafabd06 962 - bulk connectors operations: code review fixes 2023-04-26 11:22:45 -04:00
Kamila Alekbaeva
174e4d8f6f 962 - bulk connectors operations permissions for batch actions bar are fixed 2023-04-18 13:25:57 -04:00
Kamila Alekbaeva
19c0b29362 962 - bulk connectors operations delete connectors fix 2023-04-17 19:29:19 -04:00
Roman Zabaluev
8a8fedf11e
Merge branch 'master' into bulk-connectors-ops 2023-04-14 18:25:01 +08:00
Kamila Alekbaeva
52fa539e3f 962 - bulk connectors operations linters fix 2023-04-11 13:34:17 -04:00
Kamila Alekbaeva
b9798377c4 Merge branch 'master' into bulk-connectors-ops 2023-04-11 12:18:07 -04:00
Kamila Alekbaeva
afc016a7dc 962 - bulk connectors operations linters fix 2023-04-11 12:17:09 -04:00
Kamila Alekbaeva
6cd3a445c9 962 - bulk connectors operations tests are added 2023-04-11 11:18:26 -04:00
Kamila Alekbaeva
cc71300b82 Merge branch 'master' into bulk-connectors-ops 2023-03-09 13:17:30 -05:00
Kamila Alekbaeva
f9ec61ef70 962 - bulk connectors operations 2023-03-09 13:16:03 -05:00
10 changed files with 435 additions and 40 deletions

View file

@ -51,16 +51,29 @@ const Actions: React.FC = () => {
);
const stateMutation = useUpdateConnectorState(routerProps);
const mutationParams = (action: ConnectorAction) => {
return {
clusterName: routerProps.clusterName,
connectName: routerProps.connectName,
connectorName: routerProps.connectorName,
action,
};
};
const restartConnectorHandler = () =>
stateMutation.mutateAsync(ConnectorAction.RESTART);
stateMutation.mutateAsync(mutationParams(ConnectorAction.RESTART));
const restartAllTasksHandler = () =>
stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS);
stateMutation.mutateAsync(
mutationParams(ConnectorAction.RESTART_ALL_TASKS)
);
const restartFailedTasksHandler = () =>
stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS);
stateMutation.mutateAsync(
mutationParams(ConnectorAction.RESTART_FAILED_TASKS)
);
const pauseConnectorHandler = () =>
stateMutation.mutateAsync(ConnectorAction.PAUSE);
stateMutation.mutateAsync(mutationParams(ConnectorAction.PAUSE));
const resumeConnectorHandler = () =>
stateMutation.mutateAsync(ConnectorAction.RESUME);
stateMutation.mutateAsync(mutationParams(ConnectorAction.RESUME));
return (
<S.ConnectorActionsWrapperStyled>
<Dropdown

View file

@ -138,7 +138,12 @@ describe('Actions', () => {
await userEvent.click(
screen.getByRole('menuitem', { name: 'Restart Connector' })
);
expect(restartConnector).toHaveBeenCalledWith(ConnectorAction.RESTART);
expect(restartConnector).toHaveBeenCalledWith({
action: ConnectorAction.RESTART,
clusterName: 'myCluster',
connectName: 'myConnect',
connectorName: 'myConnector',
});
});
it('calls restartAllTasks', async () => {
@ -151,9 +156,12 @@ describe('Actions', () => {
await userEvent.click(
screen.getByRole('menuitem', { name: 'Restart All Tasks' })
);
expect(restartAllTasks).toHaveBeenCalledWith(
ConnectorAction.RESTART_ALL_TASKS
);
expect(restartAllTasks).toHaveBeenCalledWith({
action: ConnectorAction.RESTART_ALL_TASKS,
clusterName: 'myCluster',
connectName: 'myConnect',
connectorName: 'myConnector',
});
});
it('calls restartFailedTasks', async () => {
@ -166,9 +174,12 @@ describe('Actions', () => {
await userEvent.click(
screen.getByRole('menuitem', { name: 'Restart Failed Tasks' })
);
expect(restartFailedTasks).toHaveBeenCalledWith(
ConnectorAction.RESTART_FAILED_TASKS
);
expect(restartFailedTasks).toHaveBeenCalledWith({
action: ConnectorAction.RESTART_FAILED_TASKS,
clusterName: 'myCluster',
connectName: 'myConnect',
connectorName: 'myConnector',
});
});
it('calls pauseConnector when pause button clicked', async () => {
@ -179,7 +190,12 @@ describe('Actions', () => {
renderComponent();
await afterClickRestartButton();
await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' }));
expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE);
expect(pauseConnector).toHaveBeenCalledWith({
action: ConnectorAction.PAUSE,
clusterName: 'myCluster',
connectName: 'myConnect',
connectorName: 'myConnector',
});
});
it('calls resumeConnector when resume button clicked', async () => {
@ -193,7 +209,12 @@ describe('Actions', () => {
renderComponent();
await afterClickRestartButton();
await userEvent.click(screen.getByRole('menuitem', { name: 'Resume' }));
expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME);
expect(resumeConnector).toHaveBeenCalledWith({
action: ConnectorAction.RESUME,
clusterName: 'myCluster',
connectName: 'myConnect',
connectorName: 'myConnector',
});
});
});
});

View file

@ -0,0 +1,192 @@
import React from 'react';
import {
Action,
ResourceType,
ConnectorAction,
Connector,
} from 'generated-sources';
import useAppParams from 'lib/hooks/useAppParams';
import { useConfirm } from 'lib/hooks/useConfirm';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
import { useIsMutating, useQueryClient } from '@tanstack/react-query';
import { ActionCanButton } from 'components/common/ActionComponent';
import {
useDeleteConnector,
useUpdateConnectorState,
} from 'lib/hooks/api/kafkaConnect';
import { Row } from '@tanstack/react-table';
import { isPermitted } from 'lib/permissions';
import { useUserInfo } from 'lib/hooks/useUserInfo';
interface BatchActionsBarProps {
rows: Row<Connector>[];
resetRowSelection(): void;
}
const BatchActionsBar: React.FC<BatchActionsBarProps> = ({
rows,
resetRowSelection,
}) => {
const confirm = useConfirm();
const selectedConnectors = rows.map(({ original }) => original);
const mutationsNumber = useIsMutating();
const isMutating = mutationsNumber > 0;
const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
const { clusterName } = routerProps;
const client = useQueryClient();
const { roles, rbacFlag } = useUserInfo();
const canPerformActionOnSelected = (action: Action) => {
return selectedConnectors.every((connector) =>
isPermitted({
roles,
resource: ResourceType.CONNECT,
action,
value: connector.name,
clusterName,
rbacFlag,
})
);
};
const canEdit = canPerformActionOnSelected(Action.EDIT);
const canDelete = canPerformActionOnSelected(Action.DELETE);
const deleteConnectorMutation = useDeleteConnector(routerProps);
const deleteConnectorsHandler = () => {
confirm(
'Are you sure you want to remove selected connectors?',
async () => {
try {
await Promise.all(
selectedConnectors.map((connector) =>
deleteConnectorMutation.mutateAsync({
clusterName,
connectName: connector.connect,
connectorName: connector.name,
})
)
);
resetRowSelection();
} catch (e) {
// do nothing;
} finally {
client.invalidateQueries(['clusters', clusterName, 'connectors']);
}
}
);
};
const stateMutation = useUpdateConnectorState(routerProps);
const updateConnector = (action: ConnectorAction, message: string) => {
confirm(message, async () => {
try {
await Promise.all(
selectedConnectors.map((connector) =>
stateMutation.mutateAsync({
clusterName,
connectName: connector.connect,
connectorName: connector.name,
action,
})
)
);
resetRowSelection();
} catch (e) {
// do nothing;
} finally {
client.invalidateQueries(['clusters', clusterName, 'connectors']);
}
});
};
const restartConnectorHandler = () => {
updateConnector(
ConnectorAction.RESTART,
'Are you sure you want to restart selected connectors?'
);
};
const restartAllTasksHandler = () =>
updateConnector(
ConnectorAction.RESTART_ALL_TASKS,
'Are you sure you want to restart all tasks in selected connectors?'
);
const restartFailedTasksHandler = () =>
updateConnector(
ConnectorAction.RESTART_FAILED_TASKS,
'Are you sure you want to restart failed tasks in selected connectors?'
);
const pauseConnectorHandler = () =>
updateConnector(
ConnectorAction.PAUSE,
'Are you sure you want to pause selected connectors?'
);
const resumeConnectorHandler = () =>
updateConnector(
ConnectorAction.RESUME,
'Are you sure you want to resume selected connectors?'
);
return (
<>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={pauseConnectorHandler}
disabled={isMutating}
canDoAction={canEdit}
>
Pause
</ActionCanButton>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={resumeConnectorHandler}
disabled={isMutating}
canDoAction={canEdit}
>
Resume
</ActionCanButton>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={restartConnectorHandler}
disabled={isMutating}
canDoAction={canEdit}
>
Restart Connector
</ActionCanButton>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={restartAllTasksHandler}
disabled={isMutating}
canDoAction={canEdit}
>
Restart All Tasks
</ActionCanButton>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={restartFailedTasksHandler}
disabled={isMutating}
canDoAction={canEdit}
>
Restart Failed Tasks
</ActionCanButton>
<ActionCanButton
buttonSize="M"
buttonType="secondary"
onClick={deleteConnectorsHandler}
disabled={isMutating}
canDoAction={canDelete}
>
Delete
</ActionCanButton>
</>
);
};
export default BatchActionsBar;

View file

@ -0,0 +1,29 @@
import React from 'react';
import { CellContext } from '@tanstack/react-table';
import { FullConnectorInfo } from 'generated-sources';
import { useNavigate } from 'react-router-dom';
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
const ConnectorCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
row: { original },
}) => {
const navigate = useNavigate();
const { name, connect } = original;
const { clusterName } = useAppParams<ClusterNameRoute>();
const path = clusterConnectConnectorPath(clusterName, connect, name);
const handleOnClick = () => navigate(path);
return (
<div
role="link"
tabIndex={0}
onClick={handleOnClick}
onKeyDown={handleOnClick}
>
{name}
</div>
);
};
export default ConnectorCell;

View file

@ -1,18 +1,19 @@
import React from 'react';
import useAppParams from 'lib/hooks/useAppParams';
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
import { ClusterNameRoute } from 'lib/paths';
import Table, { TagCell } from 'components/common/NewTable';
import { FullConnectorInfo } from 'generated-sources';
import { useConnectors } from 'lib/hooks/api/kafkaConnect';
import { ColumnDef } from '@tanstack/react-table';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import ActionsCell from './ActionsCell';
import TopicsCell from './TopicsCell';
import ConnectorCell from './ConnectorCell';
import RunningTasksCell from './RunningTasksCell';
import BatchActionsBar from './BatchActionsBar';
const List: React.FC = () => {
const navigate = useNavigate();
const { clusterName } = useAppParams<ClusterNameRoute>();
const [searchParams] = useSearchParams();
const { data: connectors } = useConnectors(
@ -22,7 +23,7 @@ const List: React.FC = () => {
const columns = React.useMemo<ColumnDef<FullConnectorInfo>[]>(
() => [
{ header: 'Name', accessorKey: 'name' },
{ header: 'Name', accessorKey: 'name', cell: ConnectorCell },
{ header: 'Connect', accessorKey: 'connect' },
{ header: 'Type', accessorKey: 'type' },
{ header: 'Plugin', accessorKey: 'connectorClass' },
@ -39,9 +40,8 @@ const List: React.FC = () => {
data={connectors || []}
columns={columns}
enableSorting
onRowClick={({ original: { connect, name } }) =>
navigate(clusterConnectConnectorPath(clusterName, connect, name))
}
batchActionsBar={BatchActionsBar}
enableRowSelection
emptyMessage="No connectors found"
/>
);

View file

@ -17,6 +17,7 @@ import {
const mockedUsedNavigate = jest.fn();
const mockDelete = jest.fn();
const mockUpdate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -61,20 +62,149 @@ describe('Connectors List', () => {
it('opens broker when row clicked', async () => {
renderComponent();
await userEvent.click(
screen.getByRole('row', {
name: 'hdfs-source-connector first SOURCE FileStreamSource a b c RUNNING 2 of 2',
})
);
await waitFor(() =>
const link = await screen.findByRole('cell', {
name: 'hdfs-source-connector',
});
await userEvent.click(link);
await waitFor(() => {
expect(mockedUsedNavigate).toBeCalledWith(
clusterConnectConnectorPath(
clusterName,
'first',
'hdfs-source-connector'
)
)
);
);
});
});
describe('Selectable rows', () => {
it('renders selectable rows', () => {
renderComponent();
expect(screen.getAllByRole('checkbox').length).toEqual(3);
});
});
describe('Batch actions bar', () => {
const getButtonByName = (name: string) =>
screen.getByRole('button', { name });
beforeEach(async () => {
(useDeleteConnector as jest.Mock).mockImplementation(() => ({
mutateAsync: mockDelete,
}));
(useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
mutateAsync: mockUpdate,
}));
renderComponent();
await userEvent.click(screen.getAllByRole('checkbox')[1]);
});
it('renders batch actions bar', () => {
expect(getButtonByName('Pause')).toBeInTheDocument();
expect(getButtonByName('Resume')).toBeInTheDocument();
expect(getButtonByName('Restart Connector')).toBeInTheDocument();
expect(getButtonByName('Restart All Tasks')).toBeInTheDocument();
expect(getButtonByName('Restart Failed Tasks')).toBeInTheDocument();
expect(getButtonByName('Delete')).toBeInTheDocument();
});
it('handles delete button click', async () => {
const button = getButtonByName('Delete');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to remove selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockDelete).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockDelete).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
it('handles pause button click', async () => {
const button = getButtonByName('Pause');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to pause selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockUpdate).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
it('handles resume button click', async () => {
const button = getButtonByName('Resume');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to resume selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockUpdate).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
it('handles restart connector button click', async () => {
const button = getButtonByName('Restart Connector');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to restart selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockUpdate).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
it('handles restart all tasks button click', async () => {
const button = getButtonByName('Restart All Tasks');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to restart all tasks in selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockUpdate).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
it('handles restart failed tasks button click', async () => {
const button = getButtonByName('Restart Failed Tasks');
await userEvent.click(button);
expect(
screen.getByText(
'Are you sure you want to restart failed tasks in selected connectors?'
)
).toBeInTheDocument();
const confirmBtn = getButtonByName('Confirm');
expect(mockUpdate).not.toHaveBeenCalled();
await userEvent.click(confirmBtn);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole('checkbox')[1]).not.toBeChecked();
expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked();
});
});
});

View file

@ -15,12 +15,12 @@ import { ActionCanButton } from 'components/common/ActionComponent';
import { isPermitted } from 'lib/permissions';
import { useUserInfo } from 'lib/hooks/useUserInfo';
interface BatchActionsbarProps {
interface BatchActionsBarProps {
rows: Row<Topic>[];
resetRowSelection(): void;
}
const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
const BatchActionsBar: React.FC<BatchActionsBarProps> = ({
rows,
resetRowSelection,
}) => {
@ -166,4 +166,4 @@ const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
);
};
export default BatchActionsbar;
export default BatchActionsBar;

View file

@ -11,7 +11,7 @@ import { PER_PAGE } from 'lib/constants';
import { TopicTitleCell } from './TopicTitleCell';
import ActionsCell from './ActionsCell';
import BatchActionsbar from './BatchActionsBar';
import BatchActionsBar from './BatchActionsBar';
const TopicTable: React.FC = () => {
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
@ -101,7 +101,7 @@ const TopicTable: React.FC = () => {
columns={columns}
enableSorting
serverSideProcessing
batchActionsBar={BatchActionsbar}
batchActionsBar={BatchActionsBar}
enableRowSelection={
!isReadOnly ? (row) => !row.original.internal : undefined
}

View file

@ -91,7 +91,14 @@ describe('kafkaConnect hooks', () => {
() => hooks.useUpdateConnectorState(connectorProps),
{ wrapper: TestQueryClientProvider }
);
await act(() => result.current.mutateAsync(action));
await act(() => {
result.current.mutateAsync({
clusterName,
connectName,
connectorName,
action,
});
});
await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
expect(mock.calls()).toHaveLength(1);
});

View file

@ -1,8 +1,8 @@
import {
Connect,
Connector,
ConnectorAction,
NewConnector,
UpdateConnectorStateRequest,
} from 'generated-sources';
import { kafkaConnectApiClient as api } from 'lib/api';
import sortBy from 'lodash/sortBy';
@ -74,7 +74,7 @@ export function useConnectorTasks(props: UseConnectorProps) {
export function useUpdateConnectorState(props: UseConnectorProps) {
const client = useQueryClient();
return useMutation(
(action: ConnectorAction) => api.updateConnectorState({ ...props, action }),
(message: UpdateConnectorStateRequest) => api.updateConnectorState(message),
{
onSuccess: () =>
client.invalidateQueries(['clusters', props.clusterName, 'connectors']),
@ -136,7 +136,10 @@ export function useCreateConnector(clusterName: ClusterName) {
export function useDeleteConnector(props: UseConnectorProps) {
const client = useQueryClient();
return useMutation(() => api.deleteConnector(props), {
onSuccess: () => client.invalidateQueries(connectorsKey(props.clusterName)),
});
return useMutation(
(message: UseConnectorProps) => api.deleteConnector(message),
{
onSuccess: () => client.invalidateQueries(connectorKey(props)),
}
);
}