Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#962 Bulk connectors ops #3473

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,25 @@ const Actions: React.FC = () => {
);

const stateMutation = useUpdateConnectorState(routerProps);
const mutationParams = (action: ConnectorAction) => {
return {
clusterName: routerProps.clusterName,
connectName: routerProps.connectName,
connectorName: routerProps.connectorName,
action: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -152,7 +157,12 @@ describe('Actions', () => {
screen.getByRole('menuitem', { name: 'Restart All Tasks' })
);
expect(restartAllTasks).toHaveBeenCalledWith(
ConnectorAction.RESTART_ALL_TASKS
{
'action': ConnectorAction.RESTART_ALL_TASKS,
'clusterName': 'myCluster',
'connectName': 'myConnect',
"connectorName": 'myConnector'
}
);
});

Expand All @@ -167,7 +177,12 @@ describe('Actions', () => {
screen.getByRole('menuitem', { name: 'Restart Failed Tasks' })
);
expect(restartFailedTasks).toHaveBeenCalledWith(
ConnectorAction.RESTART_FAILED_TASKS
{
'action': ConnectorAction.RESTART_FAILED_TASKS,
'clusterName': 'myCluster',
'connectName': 'myConnect',
"connectorName": 'myConnector'
}
);
});

Expand All @@ -178,8 +193,15 @@ describe('Actions', () => {
}));
renderComponent();
await afterClickRestartButton();
await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' }));
expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE);
await userEvent.click(screen.getByRole('menuitem', {name: 'Pause'}));
expect(pauseConnector).toHaveBeenCalledWith(
{
'action': ConnectorAction.PAUSE,
'clusterName': 'myCluster',
'connectName': 'myConnect',
"connectorName": 'myConnector'
}
);
});

it('calls resumeConnector when resume button clicked', async () => {
Expand All @@ -193,7 +215,14 @@ 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'
}
);
});
});
});
Expand Down
158 changes: 158 additions & 0 deletions kafka-ui-react-app/src/components/Connect/List/BatchActionsBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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 {clusterConnectorsPath, RouterParamsClusterConnectConnector} from 'lib/paths';
import {useIsMutating} from '@tanstack/react-query';
import {ActionCanButton} from 'components/common/ActionComponent';
import { usePermission } from 'lib/hooks/usePermission';
import {useDeleteConnector, useUpdateConnectorState} from 'lib/hooks/api/kafkaConnect';
import {useNavigate} from 'react-router-dom';
import {Row} from '@tanstack/react-table';
import { useQueryClient } from '@tanstack/react-query';

interface BatchActionsBarProps {
rows: Row<Connector>[];
resetRowSelection(): void;
}

const BatchActionsBar: React.FC<BatchActionsBarProps> = ({
rows,
resetRowSelection,
}) => {

const confirm = useConfirm();
const navigate = useNavigate();

const selectedConnectors = rows.map(({ original }) => original);

const mutationsNumber = useIsMutating();
const isMutating = mutationsNumber > 0;

const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
const clusterName = routerProps.clusterName;
const client = useQueryClient();

const canEdit = usePermission(
ResourceType.CONNECT,
Action.EDIT,
routerProps.connectorName
);
const canDelete = usePermission(
ResourceType.CONNECT,
Action.DELETE,
routerProps.connectorName
);

const deleteConnectorMutation = useDeleteConnector(routerProps);
const deleteConnectorHandler = () =>
confirm(
<>
Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
connector?
</>,
async () => {
try {
await deleteConnectorMutation.mutateAsync();
navigate(clusterConnectorsPath(clusterName));
} catch {
// do not redirect
}
}
);

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<ActionButton please check this component , because the <ActionCanButton is just so we would be able to use some very custom selectors together.

use <ActionButton and it has a permisson props , please check that.

so we will have a uniform way for doing the permission.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActionButton is only capable on checking permissions on one entity, but collection. I need either modify the way we operate permissions in ActionButton or use ActionCanButton or I can create new component ActionOnCollectionButton and implement those permissions use there. WDYT is the best way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mgrdich any thoughts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will think about it and get back to you.

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={deleteConnectorHandler}
disabled={isMutating}
canDoAction={canDelete}
>
Delete
</ActionCanButton>
</>
);
};

export default BatchActionsBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { CellContext } from '@tanstack/react-table';
import {FullConnectorInfo} from 'generated-sources';
import { NavLink, useNavigate, useSearchParams } from 'react-router-dom';
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
import { LinkCell } from 'components/common/NewTable';


const ConnectorCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
row: { original },
}) => {
const navigate = useNavigate();
const { clusterName } = useAppParams<ClusterNameRoute>();
const { name, connect } = original;
const path = clusterConnectConnectorPath(clusterName, connect, name);
return (
<LinkCell
onClick={() => navigate(path)}
value={name}
to={path}
/>
// <div title={name} onClick={() => navigate(path)}>
// {name}
// </div>
);
};

export default ConnectorCell;
18 changes: 9 additions & 9 deletions kafka-ui-react-app/src/components/Connect/List/List.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React from 'react';
import useAppParams from 'lib/hooks/useAppParams';
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
import Table, { TagCell } from 'components/common/NewTable';
import { FullConnectorInfo } from 'generated-sources';
import { ClusterNameRoute } from 'lib/paths';
import Table, { TagCell, LinkCell } 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(
Expand All @@ -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' },
Expand All @@ -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={true}
emptyMessage="No connectors found"
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import userEvent from '@testing-library/user-event';
import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';
import { useConnectors, useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
import { BrowserRouter } from "react-router-dom";

const mockedUsedNavigate = jest.fn();
const mockDelete = jest.fn();
Expand All @@ -22,6 +23,7 @@ jest.mock('react-router-dom', () => ({
jest.mock('lib/hooks/api/kafkaConnect', () => ({
useConnectors: jest.fn(),
useDeleteConnector: jest.fn(),
useUpdateConnectorState: jest.fn(),
}));

const clusterName = 'local';
Expand Down Expand Up @@ -52,11 +54,8 @@ 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 userEvent.click(screen.getByRole('link', { name: 'hdfs-source-connector' }));
await waitFor(() =>
expect(mockedUsedNavigate).toBeCalledWith(
clusterConnectConnectorPath(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,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,
}) => {
Expand Down Expand Up @@ -148,4 +148,4 @@ const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
);
};

export default BatchActionsbar;
export default BatchActionsBar;
Loading