Kaynağa Gözat

962 - bulk connectors operations

Kamila Alekbaeva 2 yıl önce
ebeveyn
işleme
f9ec61ef70

+ 14 - 5
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

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

+ 35 - 6
kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx

@@ -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 () => {
@@ -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'
+          }
         );
       });
 
@@ -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'
+          }
         );
       });
 
@@ -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 () => {
@@ -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'
+          }
+        );
       });
     });
   });

+ 158 - 0
kafka-ui-react-app/src/components/Connect/List/BatchActionsBar.tsx

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

+ 29 - 0
kafka-ui-react-app/src/components/Connect/List/ConnectorCell.tsx

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

+ 9 - 9
kafka-ui-react-app/src/components/Connect/List/List.tsx

@@ -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(
@@ -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={true}
       emptyMessage="No connectors found"
     />
   );

+ 4 - 5
kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx

@@ -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();
@@ -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';
@@ -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(

+ 3 - 3
kafka-ui-react-app/src/components/Topics/List/BatchActionsBar.tsx

@@ -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,
 }) => {
@@ -148,4 +148,4 @@ const BatchActionsbar: React.FC<BatchActionsbarProps> = ({
   );
 };
 
-export default BatchActionsbar;
+export default BatchActionsBar;

+ 2 - 2
kafka-ui-react-app/src/components/Topics/List/TopicTable.tsx

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

+ 10 - 1
kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts

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

+ 2 - 7
kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts

@@ -1,9 +1,4 @@
-import {
-  Connect,
-  Connector,
-  ConnectorAction,
-  NewConnector,
-} from 'generated-sources';
+import {Connect, Connector, NewConnector, UpdateConnectorStateRequest} from 'generated-sources';
 import { kafkaConnectApiClient as api } from 'lib/api';
 import sortBy from 'lodash/sortBy';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -74,7 +69,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(connectorKey(props)),
     }