New Dropdown component (#2355)

* New Dropdown component

* Cleanup

* Styling

* Minireset
This commit is contained in:
Oleg Shur 2022-07-31 15:38:44 +03:00 committed by GitHub
parent bff27f1b5b
commit 70414d2279
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 219 additions and 278 deletions

View file

@ -11,7 +11,7 @@
"@hookform/error-message": "^2.0.0", "@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.7.1", "@hookform/resolvers": "^2.7.1",
"@reduxjs/toolkit": "^1.8.3", "@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", "@tanstack/react-query": "^4.0.5",
"@testing-library/react": "^13.2.0", "@testing-library/react": "^13.2.0",
"@types/testing-library__jest-dom": "^5.14.5", "@types/testing-library__jest-dom": "^5.14.5",

View file

@ -13,7 +13,7 @@ specifiers:
'@jest/types': ^28.1.1 '@jest/types': ^28.1.1
'@openapitools/openapi-generator-cli': ^2.5.1 '@openapitools/openapi-generator-cli': ^2.5.1
'@reduxjs/toolkit': ^1.8.3 '@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 '@tanstack/react-query': ^4.0.5
'@testing-library/dom': ^8.11.1 '@testing-library/dom': ^8.11.1
'@testing-library/jest-dom': ^5.16.4 '@testing-library/jest-dom': ^5.16.4
@ -97,7 +97,7 @@ dependencies:
'@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm '@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm
'@hookform/resolvers': 2.8.9_react-hook-form@7.6.9 '@hookform/resolvers': 2.8.9_react-hook-form@7.6.9
'@reduxjs/toolkit': 1.8.3_ctm756ikdwcjcvyfxxwskzbr6q '@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 '@tanstack/react-query': 4.0.5_ef5jwxihqo6n7gxfmzogljlgcm
'@testing-library/react': 13.2.0_ef5jwxihqo6n7gxfmzogljlgcm '@testing-library/react': 13.2.0_ef5jwxihqo6n7gxfmzogljlgcm
'@types/testing-library__jest-dom': 5.14.5 '@types/testing-library__jest-dom': 5.14.5
@ -2352,14 +2352,6 @@ packages:
reselect: 4.1.5 reselect: 4.1.5
dev: false 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: /@rushstack/eslint-patch/1.1.3:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==} resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true dev: true
@ -2377,6 +2369,18 @@ packages:
dependencies: dependencies:
'@sinonjs/commons': 1.8.3 '@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: /@tanstack/query-core/4.0.5:
resolution: {integrity: sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA==} resolution: {integrity: sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA==}
dev: false dev: false
@ -6752,6 +6756,16 @@ packages:
react: 18.1.0 react: 18.1.0
dev: false 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: /react/18.1.0:
resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==} resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,5 @@
import styled from 'styled-components'; 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' })<{ export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
leader?: boolean; leader?: boolean;
}>` }>`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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` import '@szhsin/react-menu/dist/core.css';
display: flex;
align-self: center; const menuShow = keyframes`
from {
opacity: 0;
}
`; `;
const menuHide = keyframes`
export const Trigger = styled.button.attrs({ to {
type: 'button', opacity: 0;
ariaHaspopup: 'true',
ariaControls: 'dropdown-menu',
})`
background: transparent;
border: none;
display: flex;
align-items: 'center';
justify-content: 'center';
&:hover {
cursor: pointer;
} }
`; `;
export const Item = styled.a.attrs({ export const Dropdown = styled(ControlledMenu)(
href: '#end', ({ theme: { dropdown } }) => css`
role: 'menuitem', // container for the menu items
type: 'button', ${menuSelector.name} {
})<{ $isDanger: boolean }>` border: 1px solid ${dropdown.borderColor};
color: ${({ $isDanger, theme }) => box-shadow: 0px 4px 16px ${dropdown.shadow};
$isDanger ? theme.dropdown.color : 'initial'}; padding: 8px 0;
border-radius: 4px;
font-size: 14px;
background-color: ${dropdown.backgroundColor};
text-align: left;
}
${menuSelector.stateOpening} {
animation: ${menuShow} 0.15s ease-out;
}
// 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;
cursor: pointer;
`;
export const DangerItem = styled.div`
color: ${({ theme: { dropdown } }) => dropdown.item.color.danger};
`;
export const DropdownItemHint = styled.div`
color: ${({ theme }) => theme.topicMetaData.color.label};
font-size: 12px;
margin-top: 5px;
`; `;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,7 +77,18 @@ const theme = {
breadcrumb: Colors.neutral[30], breadcrumb: Colors.neutral[30],
connectEditWarning: Colors.yellow[10], connectEditWarning: Colors.yellow[10],
dropdown: { 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: { ksqlDb: {
query: { query: {