Repository: provectus/kafka-ui
Issue: Redesign connector actions
Labels: type/enhancement
scope/frontend
status/accepted

1. one dropdown button for restart actions
2. a "triple dot" sandwich menu for deleting a connector
https://www.figma.com/file/ZkvysdRUmt9p2PDNU3a9WU/Kafka-UI?node-id=3302%3A12035&t=uAxXU6AP0QAqiwUE-0

Issue comments:

Pull request review comments:
are we using this function ? cause it seems to be deleted 
I added a new function because there pops up a warning that 'button cannot be located inside another button'
in the dropdown, I just added a div with onClick and now it works correctly and there is no warning in the tests

and yes we are using this function because the old drop down still works for deletion

i don't think the click dropdown button should check how many dropdowns we have, now it is just clicking the first or the second button.

and if this click is for the delete , we should fetch it with its text Delete , as it is done in the Restart. and we can rename the function `afterClickDeleteButton`
ok i will remove expect(dropDownButton.length).toEqual(2) this line 
let catch this with its name as well instead of `screen.getAllByRole('button');` like `screen.getByText('Restart Failed Tasks');` , so it won't fail in the future if the order of the dropdown elements changes
there is a no name button it's just a drop down button 

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts
new file mode 100644
index 00000000000..a2998acdc21
--- /dev/null
+++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Action.styled.ts
@@ -0,0 +1,23 @@
+import styled from 'styled-components';
+
+export const ConnectorActionsWrapperStyled = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+`;
+export const ButtonLabel = styled.span`
+  margin-right: 11.5px;
+`;
+export const RestartButton = styled.div`
+  padding: 0 12px;
+  border: none;
+  border-radius: 4px;
+  display: flex;
+  -webkit-align-items: center;
+  background: #e8e8fc;
+  color: #171a1c;
+  font-size: 14px;
+  font-weight: 500;
+  height: 32px;
+`;
diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
index f475b739e12..530f0db1471 100644
--- a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
+++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
@@ -1,5 +1,4 @@
 import React from 'react';
-import styled from 'styled-components';
 import { useNavigate } from 'react-router-dom';
 import { useIsMutating } from '@tanstack/react-query';
 import {
@@ -21,13 +20,9 @@ import {
 import { useConfirm } from 'lib/hooks/useConfirm';
 import { Dropdown } from 'components/common/Dropdown';
 import { ActionDropdownItem } from 'components/common/ActionComponent';
+import ChevronDownIcon from 'components/common/Icons/ChevronDownIcon';
 
-const ConnectorActionsWrapperStyled = styled.div`
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 8px;
-`;
+import * as S from './Action.styled';
 
 const Actions: React.FC = () => {
   const navigate = useNavigate();
@@ -66,10 +61,16 @@ const Actions: React.FC = () => {
     stateMutation.mutateAsync(ConnectorAction.PAUSE);
   const resumeConnectorHandler = () =>
     stateMutation.mutateAsync(ConnectorAction.RESUME);
-
   return (
-    <ConnectorActionsWrapperStyled>
-      <Dropdown>
+    <S.ConnectorActionsWrapperStyled>
+      <Dropdown
+        label={
+          <S.RestartButton>
+            <S.ButtonLabel>Restart</S.ButtonLabel>
+            <ChevronDownIcon />
+          </S.RestartButton>
+        }
+      >
         {connector?.status.state === ConnectorState.RUNNING && (
           <ActionDropdownItem
             onClick={pauseConnectorHandler}
@@ -129,6 +130,8 @@ const Actions: React.FC = () => {
         >
           Restart Failed Tasks
         </ActionDropdownItem>
+      </Dropdown>
+      <Dropdown>
         <ActionDropdownItem
           onClick={deleteConnectorHandler}
           disabled={isMutating}
@@ -142,7 +145,7 @@ const Actions: React.FC = () => {
           Delete
         </ActionDropdownItem>
       </Dropdown>
-    </ConnectorActionsWrapperStyled>
+    </S.ConnectorActionsWrapperStyled>
   );
 };
 
diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
index c3c4cff8f1a..9dce7507f59 100644
--- a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
+++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
@@ -35,8 +35,11 @@ const expectActionButtonsExists = () => {
 };
 const afterClickDropDownButton = async () => {
   const dropDownButton = screen.getAllByRole('button');
-  expect(dropDownButton.length).toEqual(1);
-  await userEvent.click(dropDownButton[0]);
+  await userEvent.click(dropDownButton[1]);
+};
+const afterClickRestartButton = async () => {
+  const dropDownButton = screen.getByText('Restart');
+  await userEvent.click(dropDownButton);
 };
 describe('Actions', () => {
   afterEach(() => {
@@ -66,8 +69,8 @@ describe('Actions', () => {
         data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),
       }));
       renderComponent();
-      await afterClickDropDownButton();
-      expect(screen.getAllByRole('menuitem').length).toEqual(5);
+      await afterClickRestartButton();
+      expect(screen.getAllByRole('menuitem').length).toEqual(4);
       expect(screen.getByText('Resume')).toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
       expectActionButtonsExists();
@@ -78,8 +81,8 @@ describe('Actions', () => {
         data: set({ ...connector }, 'status.state', ConnectorState.FAILED),
       }));
       renderComponent();
-      await afterClickDropDownButton();
-      expect(screen.getAllByRole('menuitem').length).toEqual(4);
+      await afterClickRestartButton();
+      expect(screen.getAllByRole('menuitem').length).toEqual(3);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
       expectActionButtonsExists();
@@ -90,8 +93,8 @@ describe('Actions', () => {
         data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED),
       }));
       renderComponent();
-      await afterClickDropDownButton();
-      expect(screen.getAllByRole('menuitem').length).toEqual(4);
+      await afterClickRestartButton();
+      expect(screen.getAllByRole('menuitem').length).toEqual(3);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
       expectActionButtonsExists();
@@ -102,8 +105,8 @@ describe('Actions', () => {
         data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),
       }));
       renderComponent();
-      await afterClickDropDownButton();
-      expect(screen.getAllByRole('menuitem').length).toEqual(5);
+      await afterClickRestartButton();
+      expect(screen.getAllByRole('menuitem').length).toEqual(4);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.getByText('Pause')).toBeInTheDocument();
       expectActionButtonsExists();
@@ -131,7 +134,7 @@ describe('Actions', () => {
           mutateAsync: restartConnector,
         }));
         renderComponent();
-        await afterClickDropDownButton();
+        await afterClickRestartButton();
         await userEvent.click(
           screen.getByRole('menuitem', { name: 'Restart Connector' })
         );
@@ -144,7 +147,7 @@ describe('Actions', () => {
           mutateAsync: restartAllTasks,
         }));
         renderComponent();
-        await afterClickDropDownButton();
+        await afterClickRestartButton();
         await userEvent.click(
           screen.getByRole('menuitem', { name: 'Restart All Tasks' })
         );
@@ -159,7 +162,7 @@ describe('Actions', () => {
           mutateAsync: restartFailedTasks,
         }));
         renderComponent();
-        await afterClickDropDownButton();
+        await afterClickRestartButton();
         await userEvent.click(
           screen.getByRole('menuitem', { name: 'Restart Failed Tasks' })
         );
@@ -174,7 +177,7 @@ describe('Actions', () => {
           mutateAsync: pauseConnector,
         }));
         renderComponent();
-        await afterClickDropDownButton();
+        await afterClickRestartButton();
         await userEvent.click(screen.getByRole('menuitem', { name: 'Pause' }));
         expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE);
       });
@@ -188,7 +191,7 @@ describe('Actions', () => {
           mutateAsync: resumeConnector,
         }));
         renderComponent();
-        await afterClickDropDownButton();
+        await afterClickRestartButton();
         await userEvent.click(screen.getByRole('menuitem', { name: 'Resume' }));
         expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME);
       });
diff --git a/kafka-ui-react-app/src/components/common/Button/Button.styled.ts b/kafka-ui-react-app/src/components/common/Button/Button.styled.ts
index 574e8a6c5eb..4cf793a5ed4 100644
--- a/kafka-ui-react-app/src/components/common/Button/Button.styled.ts
+++ b/kafka-ui-react-app/src/components/common/Button/Button.styled.ts
@@ -58,8 +58,10 @@ const StyledButton = styled.button<ButtonProps>`
     color: ${(props) => props.theme.button.primary.color};
   }
 
-  & svg {
-    margin-right: 7px;
+  & :first-of-type {
+    svg {
+      margin-right: 7px;
+    }
   }
 `;
 
diff --git a/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx
new file mode 100644
index 00000000000..d9bf102474d
--- /dev/null
+++ b/kafka-ui-react-app/src/components/common/Icons/ChevronDownIcon.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { useTheme } from 'styled-components';
+
+const ChevronDownIcon: React.FC = () => {
+  const theme = useTheme();
+  return (
+    <svg
+      width="10"
+      height="6"
+      viewBox="0 0 10 6"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M0.646447 0.646447C0.841709 0.451184 1.15829 0.451184 1.35355 0.646447L5 4.29289L8.64645 0.646447C8.84171 0.451184 9.15829 0.451184 9.35355 0.646447C9.54882 0.841709 9.54882 1.15829 9.35355 1.35355L5.35355 5.35355C5.15829 5.54882 4.84171 5.54882 4.64645 5.35355L0.646447 1.35355C0.451184 1.15829 0.451184 0.841709 0.646447 0.646447Z"
+        fill={theme.icons.chevronDownIcon}
+      />
+    </svg>
+  );
+};
+
+export default ChevronDownIcon;
diff --git a/kafka-ui-react-app/src/theme/theme.ts b/kafka-ui-react-app/src/theme/theme.ts
index 9eb8ac3fc79..73da7bf84d0 100644
--- a/kafka-ui-react-app/src/theme/theme.ts
+++ b/kafka-ui-react-app/src/theme/theme.ts
@@ -547,6 +547,7 @@ const theme = {
     },
   },
   icons: {
+    chevronDownIcon: Colors.neutral[90],
     closeIcon: Colors.neutral[30],
     deleteIcon: Colors.red[20],
     warningIcon: Colors.yellow[20],


Test patch: