浏览代码

New Dropdown component (#2355)

* New Dropdown component

* Cleanup

* Styling

* Minireset
Oleg Shur 2 年之前
父节点
当前提交
70414d2279
共有 24 个文件被更改,包括 217 次插入276 次删除
  1. 1 1
      kafka-ui-react-app/package.json
  2. 24 10
      kafka-ui-react-app/pnpm-lock.yaml
  3. 2 4
      kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx
  4. 2 4
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  5. 2 4
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  6. 2 4
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  7. 8 8
      kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx
  8. 0 7
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts
  9. 29 38
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  10. 2 4
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx
  11. 2 4
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  12. 2 2
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx
  13. 8 5
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  14. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
  15. 65 22
      kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts
  16. 34 34
      kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx
  17. 0 5
      kafka-ui-react-app/src/components/common/Dropdown/DropdownDivider.tsx
  18. 14 13
      kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx
  19. 0 81
      kafka-ui-react-app/src/components/common/Dropdown/__tests__/Dropdown.spec.tsx
  20. 0 20
      kafka-ui-react-app/src/components/common/Dropdown/__tests__/DropdownItem.spec.tsx
  21. 5 0
      kafka-ui-react-app/src/components/common/Dropdown/index.ts
  22. 1 4
      kafka-ui-react-app/src/theme/index.scss
  23. 1 0
      kafka-ui-react-app/src/theme/minireset.css
  24. 12 1
      kafka-ui-react-app/src/theme/theme.ts

+ 1 - 1
kafka-ui-react-app/package.json

@@ -11,7 +11,7 @@
     "@hookform/error-message": "^2.0.0",
     "@hookform/resolvers": "^2.7.1",
     "@reduxjs/toolkit": "^1.8.3",
-    "@rooks/use-outside-click-ref": "^4.10.1",
+    "@szhsin/react-menu": "^3.1.1",
     "@tanstack/react-query": "^4.0.5",
     "@testing-library/react": "^13.2.0",
     "@types/testing-library__jest-dom": "^5.14.5",

+ 24 - 10
kafka-ui-react-app/pnpm-lock.yaml

@@ -13,7 +13,7 @@ specifiers:
   '@jest/types': ^28.1.1
   '@openapitools/openapi-generator-cli': ^2.5.1
   '@reduxjs/toolkit': ^1.8.3
-  '@rooks/use-outside-click-ref': ^4.10.1
+  '@szhsin/react-menu': ^3.1.1
   '@tanstack/react-query': ^4.0.5
   '@testing-library/dom': ^8.11.1
   '@testing-library/jest-dom': ^5.16.4
@@ -97,7 +97,7 @@ dependencies:
   '@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm
   '@hookform/resolvers': 2.8.9_react-hook-form@7.6.9
   '@reduxjs/toolkit': 1.8.3_ctm756ikdwcjcvyfxxwskzbr6q
-  '@rooks/use-outside-click-ref': 4.11.2_react@18.1.0
+  '@szhsin/react-menu': 3.1.1_ef5jwxihqo6n7gxfmzogljlgcm
   '@tanstack/react-query': 4.0.5_ef5jwxihqo6n7gxfmzogljlgcm
   '@testing-library/react': 13.2.0_ef5jwxihqo6n7gxfmzogljlgcm
   '@types/testing-library__jest-dom': 5.14.5
@@ -2352,14 +2352,6 @@ packages:
       reselect: 4.1.5
     dev: false
 
-  /@rooks/use-outside-click-ref/4.11.2_react@18.1.0:
-    resolution: {integrity: sha512-w2bCW69zcpLh0KmN/odAuBsQ3sps+73KEu7zMOi0o4YMfDo+tXcqwlTJiLYysd0BEoQC9pNIklzZmI9zZep69g==}
-    peerDependencies:
-      react: '>=16.8.0'
-    dependencies:
-      react: 18.1.0
-    dev: false
-
   /@rushstack/eslint-patch/1.1.3:
     resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
     dev: true
@@ -2377,6 +2369,18 @@ packages:
     dependencies:
       '@sinonjs/commons': 1.8.3
 
+  /@szhsin/react-menu/3.1.1_ef5jwxihqo6n7gxfmzogljlgcm:
+    resolution: {integrity: sha512-IdHLyH61M+KqjTrvqglKo7JnbC0GIkg4OCtlXBxQPEjx/ecR5g0Iycqm+SG3rObEoniLZEz32iJkefve/LAHMA==}
+    peerDependencies:
+      react: '>=16.14.0'
+      react-dom: '>=16.14.0'
+    dependencies:
+      prop-types: 15.8.1
+      react: 18.1.0
+      react-dom: 18.1.0_react@18.1.0
+      react-transition-state: 1.1.4_ef5jwxihqo6n7gxfmzogljlgcm
+    dev: false
+
   /@tanstack/query-core/4.0.5:
     resolution: {integrity: sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA==}
     dev: false
@@ -6752,6 +6756,16 @@ packages:
       react: 18.1.0
     dev: false
 
+  /react-transition-state/1.1.4_ef5jwxihqo6n7gxfmzogljlgcm:
+    resolution: {integrity: sha512-6nQLWWx95gYazCm6OdtD1zGbRiirvVXPrDtHAGsYb4xs9spMM7bA8Vx77KCpjL8PJ8qz1lXFGz2PTboCSvt7iw==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+    dependencies:
+      react: 18.1.0
+      react-dom: 18.1.0_react@18.1.0
+    dev: false
+
   /react/18.1.0:
     resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==}
     engines: {node: '>=0.10.0'}

+ 2 - 4
kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx

@@ -7,11 +7,9 @@ import {
 } from 'lib/hooks/api/kafkaConnect';
 import useAppParams from 'lib/hooks/useAppParams';
 import { RouterParamsClusterConnectConnector } from 'lib/paths';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import getTagColor from 'components/common/Tag/getTagColor';
 import { Tag } from 'components/common/Tag/Tag.styled';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 const Tasks: React.FC = () => {
   const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
@@ -50,7 +48,7 @@ const Tasks: React.FC = () => {
             <td>{task.status.trace || 'null'}</td>
             <td style={{ width: '5%' }}>
               <div>
-                <Dropdown label={<VerticalElipsisIcon />} right>
+                <Dropdown>
                   <DropdownItem
                     onClick={() => restartTaskHandler(task.id?.task)}
                     danger

+ 2 - 4
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -3,15 +3,13 @@ import { FullConnectorInfo } from 'generated-sources';
 import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
 import { ClusterName } from 'redux/interfaces';
 import { Link, NavLink } from 'react-router-dom';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import getTagColor from 'components/common/Tag/getTagColor';
 import useModal from 'lib/hooks/useModal';
 import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 import * as S from './List.styled';
 
@@ -79,7 +77,7 @@ const ListItem: React.FC<ListItemProps> = ({
       </td>
       <td>
         <div>
-          <Dropdown label={<VerticalElipsisIcon />} right up>
+          <Dropdown>
             <DropdownItem onClick={setOpen} danger>
               Remove Connector
             </DropdownItem>

+ 2 - 4
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -9,11 +9,8 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageHeading from 'components/common/PageHeading/PageHeading';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import groupBy from 'lodash/groupBy';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@@ -26,6 +23,7 @@ import {
   getAreConsumerGroupDetailsFulfilled,
 } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 import getTagColor from 'components/common/Tag/getTagColor';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 import ListItem from './ListItem';
 
@@ -72,7 +70,7 @@ const Details: React.FC = () => {
       <div>
         <PageHeading text={consumerGroupID}>
           {!isReadOnly && (
-            <Dropdown label={<VerticalElipsisIcon />} right>
+            <Dropdown>
               <DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
               <DropdownItem
                 onClick={() => setIsConfirmationModalVisible(true)}

+ 2 - 4
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -10,9 +10,6 @@ import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationM
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Button } from 'components/common/Button/Button';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
@@ -31,6 +28,7 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
 import useAppParams from 'lib/hooks/useAppParams';
 import { schemasApiClient } from 'lib/api';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 import LatestVersionItem from './LatestVersion/LatestVersionItem';
 import SchemaVersion from './SchemaVersion/SchemaVersion';
@@ -101,7 +99,7 @@ const Details: React.FC = () => {
             >
               Edit Schema
             </Button>
-            <Dropdown label={<VerticalElipsisIcon />} right>
+            <Dropdown>
               <DropdownItem
                 onClick={() => setDeleteSchemaConfirmationVisible(true)}
                 danger

+ 8 - 8
kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx

@@ -6,11 +6,8 @@ import {
 } from 'generated-sources';
 import { useAppDispatch } from 'lib/hooks/redux';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import { TableCellProps } from 'components/common/SmartTable/TableColumn';
 import { TopicWithDetailedInfo } from 'redux/interfaces';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
-import Dropdown from 'components/common/Dropdown/Dropdown';
 import ClusterContext from 'components/contexts/ClusterContext';
 import * as S from 'components/Topics/List/List.styled';
 import { ClusterNameRoute } from 'lib/paths';
@@ -22,6 +19,7 @@ import {
   recreateTopic,
 } from 'redux/reducers/topics/topicsSlice';
 import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 interface TopicsListParams {
   clusterName: string;
@@ -68,8 +66,10 @@ const ActionsCell: React.FC<
 
   const isHidden = internal || isReadOnly || !hovered;
 
-  const deleteTopicHandler = () =>
+  const deleteTopicHandler = () => {
     dispatch(deleteTopic({ clusterName, topicName: name }));
+    closeDeleteTopicModal();
+  };
 
   const clearTopicMessagesHandler = () => {
     dispatch(clearTopicMessages({ clusterName, topicName: name }));
@@ -86,20 +86,20 @@ const ActionsCell: React.FC<
     <>
       <S.ActionsContainer>
         {!isHidden && (
-          <Dropdown label={<VerticalElipsisIcon />} right>
+          <Dropdown>
             {cleanUpPolicy === CleanUpPolicy.DELETE && (
               <DropdownItem onClick={openClearMessagesModal} danger>
                 Clear Messages
               </DropdownItem>
             )}
+            <DropdownItem onClick={openRecreateTopicModal} danger>
+              Recreate Topic
+            </DropdownItem>
             {isTopicDeletionAllowed && (
               <DropdownItem onClick={openDeleteTopicModal} danger>
                 Remove Topic
               </DropdownItem>
             )}
-            <DropdownItem onClick={openRecreateTopicModal} danger>
-              Recreate Topic
-            </DropdownItem>
           </Dropdown>
         )}
       </S.ActionsContainer>

+ 0 - 7
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts

@@ -1,12 +1,5 @@
 import styled from 'styled-components';
 
-export const DropdownExtraMessage = styled.div`
-  color: ${({ theme }) => theme.topicMetaData.color.label};
-  font-size: 14px;
-  width: 100%;
-  margin-top: 10px;
-`;
-
 export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
   leader?: boolean;
 }>`

+ 29 - 38
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -13,18 +13,19 @@ import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Button } from 'components/common/Button/Button';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import styled from 'styled-components';
 import Navbar from 'components/common/Navigation/Navbar.styled';
-import * as S from 'components/Topics/Topic/Details/Details.styled';
-import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import { useAppSelector } from 'lib/hooks/redux';
 import {
   getIsTopicDeletePolicy,
   getIsTopicInternal,
 } from 'redux/reducers/topics/selectors';
 import useAppParams from 'lib/hooks/useAppParams';
+import {
+  Dropdown,
+  DropdownItem,
+  DropdownItemHint,
+} from 'components/common/Dropdown';
 
 import OverviewContainer from './Overview/OverviewContainer';
 import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer';
@@ -32,7 +33,6 @@ import SettingsContainer from './Settings/SettingsContainer';
 import Messages from './Messages/Messages';
 
 interface Props {
-  isDeleted: boolean;
   deleteTopic: (payload: {
     clusterName: ClusterName;
     topicName: TopicName;
@@ -55,7 +55,6 @@ const HeaderControlsWrapper = styled.div`
 `;
 
 const Details: React.FC<Props> = ({
-  isDeleted,
   deleteTopic,
   recreateTopic,
   clearTopicMessages,
@@ -65,15 +64,14 @@ const Details: React.FC<Props> = ({
   const isInternal = useAppSelector((state) =>
     getIsTopicInternal(state, topicName)
   );
-
   const isDeletePolicy = useAppSelector((state) =>
     getIsTopicDeletePolicy(state, topicName)
   );
 
   const navigate = useNavigate();
-  const dispatch = useAppDispatch();
   const { isReadOnly, isTopicDeletionAllowed } =
     React.useContext(ClusterContext);
+
   const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
     React.useState(false);
   const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
@@ -82,13 +80,11 @@ const Details: React.FC<Props> = ({
     isRecreateTopicConfirmationVisible,
     setRecreateTopicConfirmationVisible,
   ] = React.useState(false);
-  const deleteTopicHandler = () => deleteTopic({ clusterName, topicName });
-
-  React.useEffect(() => {
-    if (isDeleted) {
-      navigate('../..');
-    }
-  }, [isDeleted, clusterName, dispatch, navigate]);
+  const deleteTopicHandler = () => {
+    deleteTopic({ clusterName, topicName });
+    setDeleteTopicConfirmationVisible(false);
+    navigate('../..');
+  };
 
   const clearTopicMessagesHandler = () => {
     clearTopicMessages({ clusterName, topicName });
@@ -124,39 +120,37 @@ const Details: React.FC<Props> = ({
               <Route
                 index
                 element={
-                  <Dropdown label={<VerticalElipsisIcon />} right>
+                  <Dropdown>
                     <DropdownItem
                       onClick={() => navigate(clusterTopicEditRelativePath)}
                     >
                       Edit settings
-                      <S.DropdownExtraMessage>
+                      <DropdownItemHint>
                         Pay attention! This operation has
                         <br />
                         especially important consequences.
-                      </S.DropdownExtraMessage>
+                      </DropdownItemHint>
+                    </DropdownItem>
+                    <DropdownItem
+                      disabled={!isDeletePolicy}
+                      onClick={() => setClearTopicConfirmationVisible(true)}
+                      danger
+                    >
+                      Clear messages
                     </DropdownItem>
-                    {isDeletePolicy && (
-                      <DropdownItem
-                        onClick={() => setClearTopicConfirmationVisible(true)}
-                        danger
-                      >
-                        Clear messages
-                      </DropdownItem>
-                    )}
                     <DropdownItem
                       onClick={() => setRecreateTopicConfirmationVisible(true)}
                       danger
                     >
                       Recreate Topic
                     </DropdownItem>
-                    {isTopicDeletionAllowed && (
-                      <DropdownItem
-                        onClick={() => setDeleteTopicConfirmationVisible(true)}
-                        danger
-                      >
-                        Remove topic
-                      </DropdownItem>
-                    )}
+                    <DropdownItem
+                      disabled={!isTopicDeletionAllowed}
+                      onClick={() => setDeleteTopicConfirmationVisible(true)}
+                      danger
+                    >
+                      Remove Topic
+                    </DropdownItem>
                   </Dropdown>
                 }
               />
@@ -214,14 +208,11 @@ const Details: React.FC<Props> = ({
       </Navbar>
       <Routes>
         <Route index element={<OverviewContainer />} />
-
         <Route path={clusterTopicMessagesRelativePath} element={<Messages />} />
-
         <Route
           path={clusterTopicSettingsRelativePath}
           element={<SettingsContainer />}
         />
-
         <Route
           path={clusterTopicConsumerGroupsRelativePath}
           element={<TopicConsumerGroupsContainer />}

+ 2 - 4
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx

@@ -1,13 +1,11 @@
 import React from 'react';
 import dayjs from 'dayjs';
 import { TopicMessage } from 'generated-sources';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import useDataSaver from 'lib/hooks/useDataSaver';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
 import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import styled from 'styled-components';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 import MessageContent from './MessageContent/MessageContent';
 import * as S from './MessageContent/MessageContent.styled';
@@ -76,7 +74,7 @@ const Message: React.FC<Props> = ({
         </StyledDataCell>
         <td style={{ width: '5%' }}>
           {vEllipsisOpen && (
-            <Dropdown label={<VerticalElipsisIcon />} right>
+            <Dropdown>
               <DropdownItem onClick={copyToClipboard}>
                 Copy to clipboard
               </DropdownItem>

+ 2 - 4
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -1,13 +1,10 @@
 import React from 'react';
 import { Partition, Replica } from 'generated-sources';
 import { ClusterName, TopicName } from 'redux/interfaces';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import ClusterContext from 'components/contexts/ClusterContext';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { useAppSelector } from 'lib/hooks/redux';
@@ -15,6 +12,7 @@ import { getTopicByName } from 'redux/reducers/topics/selectors';
 import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
 import { RouteParamsClusterTopic } from 'lib/paths';
 import useAppParams from 'lib/hooks/useAppParams';
+import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
 export interface Props {
   clearTopicMessages(params: {
@@ -135,7 +133,7 @@ const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
                 <td>{partition.offsetMax - partition.offsetMin}</td>
                 <td style={{ width: '5%' }}>
                   {!internal && !isReadOnly && cleanUpPolicy === 'DELETE' ? (
-                    <Dropdown label={<VerticalElipsisIcon />} right>
+                    <Dropdown>
                       <DropdownItem
                         onClick={() =>
                           clearTopicMessages({

+ 2 - 2
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx

@@ -80,7 +80,7 @@ describe('Overview', () => {
   });
 
   describe('when it has internal flag', () => {
-    it('does not render the Action button a Topic', () => {
+    it('renders the Action button for Topic', () => {
       setupComponent(
         {},
         {
@@ -90,7 +90,7 @@ describe('Overview', () => {
           cleanUpPolicy: CleanUpPolicy.DELETE,
         }
       );
-      expect(screen.getAllByRole('menu')[0]).toBeInTheDocument();
+      expect(screen.getAllByLabelText('Dropdown Toggle').length).toEqual(1);
     });
 
     it('does not render Partitions', () => {

+ 8 - 5
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -46,7 +46,6 @@ describe('Details', () => {
             deleteTopic={mockDelete}
             recreateTopic={mockRecreateTopic}
             clearTopicMessages={mockClearTopicMessages}
-            isDeleted={false}
             {...props}
           />
         </WithRoute>
@@ -83,7 +82,6 @@ describe('Details', () => {
             deleteTopic={mockDelete}
             recreateTopic={mockRecreateTopic}
             clearTopicMessages={mockClearTopicMessages}
-            isDeleted={false}
           />
         </ClusterContext.Provider>
       );
@@ -95,8 +93,7 @@ describe('Details', () => {
   describe('when remove topic modal is open', () => {
     beforeEach(() => {
       setupComponent();
-
-      const openModalButton = screen.getAllByText('Remove topic')[0];
+      const openModalButton = screen.getAllByText('Remove Topic')[0];
       userEvent.click(openModalButton);
     });
 
@@ -156,7 +153,13 @@ describe('Details', () => {
   });
 
   it('redirects to the correct route if topic is deleted', () => {
-    setupComponent({ isDeleted: true });
+    setupComponent();
+
+    const deleteTopicButton = screen.getByText(/Remove topic/i);
+    userEvent.click(deleteTopicButton);
+
+    const submitDeleteButton = screen.getByText(/Submit/i);
+    userEvent.click(submitDeleteButton);
 
     expect(mockNavigate).toHaveBeenCalledWith('../..');
   });

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx

@@ -170,7 +170,7 @@ const SendMessage: React.FC = () => {
                   aria-labelledby="selectPartitionOptions"
                   name={name}
                   onChange={onChange}
-                  minWidth="100%"
+                  minWidth="100px"
                   options={selectPartitionOptions}
                   value={selectPartitionOptions[0].value}
                 />

+ 65 - 22
kafka-ui-react-app/src/components/common/Dropdown/Dropdown.styled.ts

@@ -1,30 +1,73 @@
-import styled from 'styled-components';
+import styled, { css, keyframes } from 'styled-components';
+import { ControlledMenu } from '@szhsin/react-menu';
+import { menuSelector, menuItemSelector } from '@szhsin/react-menu/style-utils';
 
-export const TriggerWrapper = styled.div`
-  display: flex;
-  align-self: center;
+import '@szhsin/react-menu/dist/core.css';
+
+const menuShow = keyframes`
+  from {
+    opacity: 0;
+  }
 `;
+const menuHide = keyframes`
+  to {
+    opacity: 0;
+  }
+`;
+
+export const Dropdown = styled(ControlledMenu)(
+  ({ theme: { dropdown } }) => css`
+    // container for the menu items
+    ${menuSelector.name} {
+      border: 1px solid ${dropdown.borderColor};
+      box-shadow: 0px 4px 16px ${dropdown.shadow};
+      padding: 8px 0;
+      border-radius: 4px;
+      font-size: 14px;
+      background-color: ${dropdown.backgroundColor};
+      text-align: left;
+    }
+
+    ${menuSelector.stateOpening} {
+      animation: ${menuShow} 0.15s ease-out;
+    }
 
-export const Trigger = styled.button.attrs({
-  type: 'button',
-  ariaHaspopup: 'true',
-  ariaControls: 'dropdown-menu',
-})`
-  background: transparent;
+    // NOTE: animation-fill-mode: forwards is required to
+    // prevent flickering with React 18 createRoot()
+    ${menuSelector.stateClosing} {
+      animation: ${menuHide} 0.2s ease-out forwards;
+    }
+
+    ${menuItemSelector.name} {
+      padding: 6px 16px;
+      min-width: 150px;
+      background-color: ${dropdown.item.backgroundColor.default};
+      white-space: nowrap;
+    }
+
+    ${menuItemSelector.hover} {
+      background-color: ${dropdown.item.backgroundColor.hover};
+    }
+
+    ${menuItemSelector.disabled} {
+      cursor: not-allowed;
+    }
+  `
+);
+
+export const DropdownButton = styled.button`
+  background-color: transparent;
   border: none;
   display: flex;
-  align-items: 'center';
-  justify-content: 'center';
-  &:hover {
-    cursor: pointer;
-  }
+  cursor: pointer;
+`;
+
+export const DangerItem = styled.div`
+  color: ${({ theme: { dropdown } }) => dropdown.item.color.danger};
 `;
 
-export const Item = styled.a.attrs({
-  href: '#end',
-  role: 'menuitem',
-  type: 'button',
-})<{ $isDanger: boolean }>`
-  color: ${({ $isDanger, theme }) =>
-    $isDanger ? theme.dropdown.color : 'initial'};
+export const DropdownItemHint = styled.div`
+  color: ${({ theme }) => theme.topicMetaData.color.label};
+  font-size: 12px;
+  margin-top: 5px;
 `;

+ 34 - 34
kafka-ui-react-app/src/components/common/Dropdown/Dropdown.tsx

@@ -1,46 +1,46 @@
-import useOutsideClickRef from '@rooks/use-outside-click-ref';
-import cx from 'classnames';
-import React, { PropsWithChildren, useMemo, useState } from 'react';
+import { MenuProps } from '@szhsin/react-menu';
+import React, { PropsWithChildren, useRef } from 'react';
+import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
+import useModal from 'lib/hooks/useModal';
 
 import * as S from './Dropdown.styled';
 
-export interface DropdownProps {
-  label: React.ReactNode;
-  right?: boolean;
-  up?: boolean;
+interface DropdownProps extends PropsWithChildren<Partial<MenuProps>> {
+  label?: React.ReactNode;
 }
 
-const Dropdown: React.FC<PropsWithChildren<DropdownProps>> = ({
-  label,
-  right,
-  up,
-  children,
-}) => {
-  const [active, setActive] = useState<boolean>(false);
-  const [wrapperRef] = useOutsideClickRef(() => setActive(false));
-  const onClick = (e: React.MouseEvent) => {
+const Dropdown: React.FC<DropdownProps> = ({ label, children }) => {
+  const ref = useRef(null);
+  const { isOpen, setClose, setOpen } = useModal(false);
+
+  const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
+    e.preventDefault();
     e.stopPropagation();
-    setActive(!active);
+    setOpen();
   };
 
-  const classNames = useMemo(
-    () =>
-      cx('dropdown', {
-        'is-active': active,
-        'is-right': right,
-        'is-up': up,
-      }),
-    [active, right, up]
-  );
   return (
-    <div className={classNames} ref={wrapperRef}>
-      <S.TriggerWrapper>
-        <S.Trigger onClick={onClick}>{label}</S.Trigger>
-      </S.TriggerWrapper>
-      <div className="dropdown-menu" id="dropdown-menu" role="menu">
-        <div className="dropdown-content has-text-left">{children}</div>
-      </div>
-    </div>
+    <>
+      <S.DropdownButton
+        onClick={handleClick}
+        ref={ref}
+        aria-label="Dropdown Toggle"
+      >
+        {label || <VerticalElipsisIcon />}
+      </S.DropdownButton>
+      <S.Dropdown
+        anchorRef={ref}
+        state={isOpen ? 'open' : 'closed'}
+        onMouseLeave={setClose}
+        onClose={setClose}
+        align="end"
+        direction="bottom"
+        offsetY={10}
+        viewScroll="auto"
+      >
+        {children}
+      </S.Dropdown>
+    </>
   );
 };
 

+ 0 - 5
kafka-ui-react-app/src/components/common/Dropdown/DropdownDivider.tsx

@@ -1,5 +0,0 @@
-import React from 'react';
-
-const DropdownDivider: React.FC = () => <hr className="dropdown-divider" />;
-
-export default DropdownDivider;

+ 14 - 13
kafka-ui-react-app/src/components/common/Dropdown/DropdownItem.tsx

@@ -1,31 +1,32 @@
 import React, { PropsWithChildren } from 'react';
+import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
 
 import * as S from './Dropdown.styled';
 
-interface DropdownItemProps {
-  onClick(): void;
+interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
   danger?: boolean;
+  onClick?(): void;
 }
 
-const DropdownItem: React.FC<PropsWithChildren<DropdownItemProps>> = ({
+const DropdownItem: React.FC<DropdownItemProps> = ({
   onClick,
   danger,
   children,
+  ...rest
 }) => {
-  const onClickHandler = (e: React.MouseEvent) => {
-    e.preventDefault();
-    e.stopPropagation();
+  const handleClick = (e: ClickEvent) => {
+    if (!onClick) return;
+
+    // eslint-disable-next-line no-param-reassign
+    e.stopPropagation = true;
+    e.syntheticEvent.stopPropagation();
     onClick();
   };
 
   return (
-    <S.Item
-      $isDanger={!!danger}
-      onClick={onClickHandler}
-      className="dropdown-item is-link"
-    >
-      {children}
-    </S.Item>
+    <MenuItem onClick={handleClick} {...rest}>
+      {danger ? <S.DangerItem>{children}</S.DangerItem> : children}
+    </MenuItem>
   );
 };
 

+ 0 - 81
kafka-ui-react-app/src/components/common/Dropdown/__tests__/Dropdown.spec.tsx

@@ -1,81 +0,0 @@
-import React from 'react';
-import Dropdown, { DropdownProps } from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
-import userEvent from '@testing-library/user-event';
-import { render } from 'lib/testHelpers';
-import { screen } from '@testing-library/react';
-
-const dummyLable = 'My Test Label';
-const dummyChildren = (
-  <>
-    <DropdownItem onClick={jest.fn()}>Child 1</DropdownItem>
-    <DropdownItem onClick={jest.fn()}>Child 2</DropdownItem>
-    <DropdownDivider />
-    <DropdownItem onClick={jest.fn()}>Child 3</DropdownItem>
-  </>
-);
-
-describe('Dropdown', () => {
-  const setupWrapper = (
-    props: Partial<DropdownProps> = {},
-    children: React.ReactNode = undefined
-  ) => (
-    <Dropdown label={dummyLable} {...props}>
-      {children}
-    </Dropdown>
-  );
-
-  it('renders Dropdown with initial props', () => {
-    const wrapper = render(setupWrapper()).baseElement;
-    expect(wrapper.querySelector('.dropdown')).toBeTruthy();
-
-    expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy();
-    expect(wrapper.querySelector('.dropdown.is-right')).toBeFalsy();
-    expect(wrapper.querySelector('.dropdown.is-up')).toBeFalsy();
-
-    expect(wrapper.querySelector('.dropdown-content')).toBeTruthy();
-    expect(wrapper.querySelector('.dropdown-content')).toHaveTextContent('');
-  });
-
-  it('renders custom children', () => {
-    const wrapper = render(setupWrapper({}, dummyChildren)).baseElement;
-    expect(wrapper.querySelector('.dropdown-content')).toBeTruthy();
-    expect(wrapper.querySelectorAll('.dropdown-item').length).toEqual(3);
-    expect(wrapper.querySelectorAll('.dropdown-divider').length).toEqual(1);
-  });
-
-  it('renders dropdown with a right-aligned menu', () => {
-    const wrapper = render(setupWrapper({ right: true })).baseElement;
-    expect(wrapper.querySelector('.dropdown.is-right')).toBeTruthy();
-  });
-
-  it('renders dropdown with a popup menu', () => {
-    const wrapper = render(setupWrapper({ up: true })).baseElement;
-    expect(wrapper.querySelector('.dropdown.is-up')).toBeTruthy();
-  });
-
-  it('handles click', () => {
-    const wrapper = render(setupWrapper()).baseElement;
-    const button = screen.getByText('My Test Label');
-
-    expect(button).toBeInTheDocument();
-    expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy();
-
-    userEvent.click(button);
-    expect(wrapper.querySelector('.dropdown.is-active')).toBeTruthy();
-  });
-
-  it('to be in the document', () => {
-    render(
-      setupWrapper(
-        {
-          right: true,
-          up: true,
-        },
-        dummyChildren
-      )
-    );
-    expect(screen.getByRole('menu')).toBeInTheDocument();
-  });
-});

+ 0 - 20
kafka-ui-react-app/src/components/common/Dropdown/__tests__/DropdownItem.spec.tsx

@@ -1,20 +0,0 @@
-import React from 'react';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import { render } from 'lib/testHelpers';
-import userEvent from '@testing-library/user-event';
-import { screen } from '@testing-library/react';
-
-const onClick = jest.fn();
-
-describe('DropdownItem', () => {
-  it('to be in the document', () => {
-    render(<DropdownItem onClick={jest.fn()}>Item 1</DropdownItem>);
-    expect(screen.getByText('Item 1')).toBeInTheDocument();
-  });
-
-  it('handles Click', () => {
-    render(<DropdownItem onClick={onClick}>Item 1</DropdownItem>);
-    userEvent.click(screen.getByText('Item 1'));
-    expect(onClick).toHaveBeenCalled();
-  });
-});

+ 5 - 0
kafka-ui-react-app/src/components/common/Dropdown/index.ts

@@ -0,0 +1,5 @@
+import { DropdownItemHint } from './Dropdown.styled';
+import Dropdown from './Dropdown';
+import DropdownItem from './DropdownItem';
+
+export { Dropdown, DropdownItem, DropdownItemHint };

+ 1 - 4
kafka-ui-react-app/src/theme/index.scss

@@ -1,12 +1,9 @@
 @import '@fortawesome/fontawesome-free/css/all.min.css';
 
 // Base
-@import "bulma/sass/base/minireset";
+@import "./minireset";
 @import "bulma/sass/base/generic";
 
-// Components
-@import "bulma/sass/components/dropdown";
-
 @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&display=swap');
 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');
 

+ 1 - 0
kafka-ui-react-app/src/theme/minireset.css

@@ -0,0 +1 @@
+/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}

+ 12 - 1
kafka-ui-react-app/src/theme/theme.ts

@@ -77,7 +77,18 @@ const theme = {
   breadcrumb: Colors.neutral[30],
   connectEditWarning: Colors.yellow[10],
   dropdown: {
-    color: Colors.red[50],
+    backgroundColor: Colors.neutral[0],
+    borderColor: Colors.neutral[5],
+    shadow: Colors.transparency[20],
+    item: {
+      color: {
+        danger: Colors.red[60],
+      },
+      backgroundColor: {
+        default: Colors.neutral[0],
+        hover: Colors.neutral[5],
+      },
+    },
   },
   ksqlDb: {
     query: {