New Confirmation Messages modal (#2376)

* New Confirmation Messages

* Fix #2348

* Fix codesmels

* fix #2242

* fix #2347
This commit is contained in:
Oleg Shur 2022-08-03 11:37:35 +03:00 committed by GitHub
parent b6e9e43868
commit 2d82b9c0a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 591 additions and 959 deletions

View file

@ -18,6 +18,9 @@ import Logo from 'components/common/Logo/Logo';
import GitIcon from 'components/common/Icons/GitIcon';
import DiscordIcon from 'components/common/Icons/DiscordIcon';
import { ConfirmContextProvider } from './contexts/ConfirmContext';
import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -44,84 +47,87 @@ const App: React.FC = () => {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<GlobalCSS />
<S.Layout>
<S.Navbar role="navigation" aria-label="Page Header">
<S.NavbarBrand>
<ConfirmContextProvider>
<GlobalCSS />
<S.Layout>
<S.Navbar role="navigation" aria-label="Page Header">
<S.NavbarBrand>
<S.NavbarBurger
onClick={onBurgerClick}
onKeyDown={onBurgerClick}
role="button"
tabIndex={0}
aria-label="burger"
>
<S.Span role="separator" />
<S.Span role="separator" />
<S.Span role="separator" />
</S.NavbarBurger>
<S.NavbarBrand>
<S.NavbarBurger
onClick={onBurgerClick}
onKeyDown={onBurgerClick}
role="button"
tabIndex={0}
aria-label="burger"
>
<S.Span role="separator" />
<S.Span role="separator" />
<S.Span role="separator" />
</S.NavbarBurger>
<S.Hyperlink to="/">
<Logo />
UI for Apache Kafka
</S.Hyperlink>
<S.Hyperlink to="/">
<Logo />
UI for Apache Kafka
</S.Hyperlink>
<S.NavbarItem>
{GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
</S.NavbarItem>
<S.NavbarItem>
{GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
</S.NavbarItem>
</S.NavbarBrand>
</S.NavbarBrand>
</S.NavbarBrand>
<S.NavbarSocial>
<S.LogoutLink href="/logout">
<S.LogoutButton buttonType="primary" buttonSize="M">
Log out
</S.LogoutButton>
</S.LogoutLink>
<S.SocialLink
href="https://github.com/provectus/kafka-ui"
target="_blank"
>
<GitIcon />
</S.SocialLink>
<S.SocialLink
href="https://discord.com/invite/4DWzD7pGE5"
target="_blank"
>
<DiscordIcon />
</S.SocialLink>
</S.NavbarSocial>
</S.Navbar>
<S.NavbarSocial>
<S.LogoutLink href="/logout">
<S.LogoutButton buttonType="primary" buttonSize="M">
Log out
</S.LogoutButton>
</S.LogoutLink>
<S.SocialLink
href="https://github.com/provectus/kafka-ui"
target="_blank"
>
<GitIcon />
</S.SocialLink>
<S.SocialLink
href="https://discord.com/invite/4DWzD7pGE5"
target="_blank"
>
<DiscordIcon />
</S.SocialLink>
</S.NavbarSocial>
</S.Navbar>
<S.Container>
<S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
<Suspense fallback={<PageLoader />}>
<Nav />
</Suspense>
</S.Sidebar>
<S.Overlay
$visible={isSidebarVisible}
onClick={closeSidebar}
onKeyDown={closeSidebar}
tabIndex={-1}
aria-hidden="true"
aria-label="Overlay"
/>
<Routes>
{['/', '/ui', '/ui/clusters'].map((path) => (
<Route
key="Home" // optional: avoid full re-renders on route changes
path={path}
element={<Dashboard />}
/>
))}
<Route
path={getNonExactPath(clusterPath())}
element={<ClusterPage />}
<S.Container>
<S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
<Suspense fallback={<PageLoader />}>
<Nav />
</Suspense>
</S.Sidebar>
<S.Overlay
$visible={isSidebarVisible}
onClick={closeSidebar}
onKeyDown={closeSidebar}
tabIndex={-1}
aria-hidden="true"
aria-label="Overlay"
/>
</Routes>
</S.Container>
<Toaster position="bottom-right" />
</S.Layout>
<Routes>
{['/', '/ui', '/ui/clusters'].map((path) => (
<Route
key="Home" // optional: avoid full re-renders on route changes
path={path}
element={<Dashboard />}
/>
))}
<Route
path={getNonExactPath(clusterPath())}
element={<ClusterPage />}
/>
</Routes>
</S.Container>
<Toaster position="bottom-right" />
</S.Layout>
<ConfirmationModal />
</ConfirmContextProvider>
</ThemeProvider>
</QueryClientProvider>
);

View file

@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
import { useIsMutating } from '@tanstack/react-query';
import { ConnectorState, ConnectorAction } from 'generated-sources';
import useAppParams from 'lib/hooks/useAppParams';
import useModal from 'lib/hooks/useModal';
import {
useConnector,
useDeleteConnector,
@ -15,8 +14,8 @@ import {
clusterConnectorsPath,
RouterParamsClusterConnectConnector,
} from 'lib/paths';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { Button } from 'components/common/Button/Button';
import { useConfirm } from 'lib/hooks/useConfirm';
const ConnectorActionsWrapperStyled = styled.div`
display: flex;
@ -32,22 +31,24 @@ const Actions: React.FC = () => {
const isMutating = mutationsNumber > 0;
const { data: connector } = useConnector(routerProps);
const {
isOpen: isDeleteConnectorConfirmationOpen,
setClose: setDeleteConnectorConfirmationClose,
setOpen: setDeleteConnectorConfirmationOpen,
} = useModal();
const confirm = useConfirm();
const deleteConnectorMutation = useDeleteConnector(routerProps);
const deleteConnectorHandler = async () => {
try {
await deleteConnectorMutation.mutateAsync();
navigate(clusterConnectorsPath(routerProps.clusterName));
} catch {
// do not redirect
}
};
const deleteConnectorHandler = () =>
confirm(
<>
Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
connector?
</>,
async () => {
try {
await deleteConnectorMutation.mutateAsync();
navigate(clusterConnectorsPath(routerProps.clusterName));
} catch {
// do not redirect
}
}
);
const stateMutation = useUpdateConnectorState(routerProps);
const restartConnectorHandler = () =>
@ -71,7 +72,7 @@ const Actions: React.FC = () => {
onClick={pauseConnectorHandler}
disabled={isMutating}
>
<span>Pause</span>
Pause
</Button>
)}
@ -83,7 +84,7 @@ const Actions: React.FC = () => {
onClick={resumeConnectorHandler}
disabled={isMutating}
>
<span>Resume</span>
Resume
</Button>
)}
@ -94,7 +95,7 @@ const Actions: React.FC = () => {
onClick={restartConnectorHandler}
disabled={isMutating}
>
<span>Restart Connector</span>
Restart Connector
</Button>
<Button
buttonSize="M"
@ -103,7 +104,7 @@ const Actions: React.FC = () => {
onClick={restartAllTasksHandler}
disabled={isMutating}
>
<span>Restart All Tasks</span>
Restart All Tasks
</Button>
<Button
buttonSize="M"
@ -112,7 +113,7 @@ const Actions: React.FC = () => {
onClick={restartFailedTasksHandler}
disabled={isMutating}
>
<span>Restart Failed Tasks</span>
Restart Failed Tasks
</Button>
<Button
buttonSize="M"
@ -125,27 +126,18 @@ const Actions: React.FC = () => {
routerProps.connectorName
)}
>
<span>Edit Config</span>
Edit Config
</Button>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={setDeleteConnectorConfirmationOpen}
onClick={deleteConnectorHandler}
disabled={isMutating}
>
<span>Delete</span>
Delete
</Button>
<ConfirmationModal
isOpen={isDeleteConnectorConfirmationOpen}
onCancel={setDeleteConnectorConfirmationClose}
onConfirm={deleteConnectorHandler}
isConfirming={isMutating}
>
Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
connector?
</ConfirmationModal>
</ConnectorActionsWrapperStyled>
);
};

View file

@ -3,7 +3,7 @@ import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorPath } from 'lib/paths';
import Actions from 'components/Connect/Details/Actions/Actions';
import { ConnectorAction, ConnectorState } from 'generated-sources';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
useConnector,
@ -27,11 +27,6 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({
useUpdateConnectorState: jest.fn(),
}));
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
const expectActionButtonsExists = () => {
expect(screen.getByText('Restart Connector')).toBeInTheDocument();
expect(screen.getByText('Restart All Tasks')).toBeInTheDocument();
@ -116,10 +111,10 @@ describe('Actions', () => {
it('opens confirmation modal when delete button clicked', async () => {
renderComponent();
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
expect(
screen.getByText(/Are you sure you want to remove/i)
).toHaveAttribute('isopen', 'true');
await waitFor(() =>
userEvent.click(screen.getByRole('button', { name: 'Delete' }))
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
it('calls restartConnector when restart button clicked', () => {

View file

@ -32,8 +32,8 @@ describe('Config', () => {
it('is empty when no config', () => {
(useConnectorConfig as jest.Mock).mockImplementation(() => ({}));
const { container } = renderComponent();
expect(container).toBeEmptyDOMElement();
renderComponent();
expect(screen.queryByText('mock-Editor')).not.toBeInTheDocument();
});
it('renders editor', () => {

View file

@ -19,8 +19,8 @@ describe('Overview', () => {
data: undefined,
}));
const { container } = render(<Overview />);
expect(container).toBeEmptyDOMElement();
render(<Overview />);
expect(screen.queryByText('Worker')).not.toBeInTheDocument();
});
describe('when connector is loaded', () => {

View file

@ -3,13 +3,12 @@ import { FullConnectorInfo } from 'generated-sources';
import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
import { ClusterName } from 'redux/interfaces';
import { Link, NavLink } from 'react-router-dom';
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 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 { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './List.styled';
@ -31,16 +30,22 @@ const ListItem: React.FC<ListItemProps> = ({
failedTasksCount,
},
}) => {
const { isOpen, setClose, setOpen } = useModal();
const confirm = useConfirm();
const deleteMutation = useDeleteConnector({
clusterName,
connectName: connect,
connectorName: name,
});
const handleDelete = async () => {
await deleteMutation.mutateAsync();
setClose();
const handleDelete = () => {
confirm(
<>
Are you sure want to remove <b>{name}</b> connector?
</>,
async () => {
await deleteMutation.mutateAsync();
}
);
};
const runningTasks = React.useMemo(() => {
@ -78,18 +83,11 @@ const ListItem: React.FC<ListItemProps> = ({
<td>
<div>
<Dropdown>
<DropdownItem onClick={setOpen} danger>
<DropdownItem onClick={handleDelete} danger>
Remove Connector
</DropdownItem>
</Dropdown>
</div>
<ConfirmationModal
isOpen={isOpen}
onCancel={setClose}
onConfirm={handleDelete}
>
Are you sure want to remove <b>{name}</b> connector?
</ConfirmationModal>
</td>
</tr>
);

View file

@ -1,16 +1,9 @@
import React from 'react';
import { connectors } from 'lib/fixtures/kafkaConnect';
import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
describe('Connectors ListItem', () => {
const connector = connectors[0];
const setupWrapper = (props: Partial<ListItemProps> = {}) => (
@ -21,25 +14,6 @@ describe('Connectors ListItem', () => {
</table>
);
const onCancel = jest.fn();
const onConfirm = jest.fn();
const confirmationModal = (props: Partial<ListItemProps> = {}) => (
<ConfirmationModal onCancel={onCancel} onConfirm={onConfirm}>
<button type="button" id="cancel" onClick={onCancel}>
Cancel
</button>
{props.clusterName ? (
<button type="button" id="delete" onClick={onConfirm}>
Confirm
</button>
) : (
<button type="button" id="delete">
Confirm
</button>
)}
</ConfirmationModal>
);
it('renders item', () => {
render(setupWrapper());
expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2');
@ -76,22 +50,4 @@ describe('Connectors ListItem', () => {
);
expect(screen.getAllByRole('cell')[6]).toHaveTextContent('');
});
it('handles cancel', async () => {
render(confirmationModal());
userEvent.click(screen.getByText('Cancel'));
expect(onCancel).toHaveBeenCalled();
});
it('handles delete', () => {
render(confirmationModal({ clusterName: 'test' }));
userEvent.click(screen.getByText('Confirm'));
expect(onConfirm).toHaveBeenCalled();
});
it('handles delete when clusterName is not present', () => {
render(confirmationModal({ clusterName: undefined }));
userEvent.click(screen.getByText('Confirm'));
expect(onConfirm).toHaveBeenCalledTimes(0);
});
});

View file

@ -6,7 +6,6 @@ import {
ClusterGroupParam,
} from 'lib/paths';
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 * as Metrics from 'components/common/Metrics';
@ -38,17 +37,14 @@ const Details: React.FC = () => {
const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
React.useState<boolean>(false);
React.useEffect(() => {
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
}, [clusterName, consumerGroupID, dispatch]);
const onDelete = () => {
setIsConfirmationModalVisible(false);
dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
};
React.useEffect(() => {
if (isDeleted) {
navigate('../');
@ -73,7 +69,8 @@ const Details: React.FC = () => {
<Dropdown>
<DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
<DropdownItem
onClick={() => setIsConfirmationModalVisible(true)}
confirm="Are you sure you want to delete this consumer group?"
onClick={onDelete}
danger
>
Delete consumer group
@ -119,13 +116,6 @@ const Details: React.FC = () => {
))}
</tbody>
</Table>
<ConfirmationModal
isOpen={isConfirmationModalVisible}
onCancel={() => setIsConfirmationModalVisible(false)}
onConfirm={onDelete}
>
Are you sure you want to delete this consumer group?
</ConfirmationModal>
</div>
);
};

View file

@ -97,12 +97,13 @@ describe('Details component', () => {
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
200
);
await act(() => {
userEvent.click(screen.getByText('Submit'));
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
});
expect(deleteConsumerGroupMock.called()).toBeTruthy();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(mockNavigate).toHaveBeenLastCalledWith('../');
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
await waitFor(() => expect(mockNavigate).toHaveBeenLastCalledWith('../'));
});
});
});

View file

@ -6,7 +6,6 @@ import {
clusterSchemaSchemaComparePageRelativePath,
} from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import PageLoader from 'components/common/PageLoader/PageLoader';
import PageHeading from 'components/common/PageHeading/PageHeading';
import { Button } from 'components/common/Button/Button';
@ -38,10 +37,6 @@ const Details: React.FC = () => {
const dispatch = useAppDispatch();
const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
const [
isDeleteSchemaConfirmationVisible,
setDeleteSchemaConfirmationVisible,
] = React.useState(false);
React.useEffect(() => {
dispatch(fetchLatestSchema({ clusterName, subject }));
@ -62,7 +57,7 @@ const Details: React.FC = () => {
const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
const onDelete = async () => {
const deleteHandler = async () => {
try {
await schemasApiClient.deleteSchema({
clusterName,
@ -101,19 +96,17 @@ const Details: React.FC = () => {
</Button>
<Dropdown>
<DropdownItem
onClick={() => setDeleteSchemaConfirmationVisible(true)}
confirm={
<>
Are you sure want to remove <b>{subject}</b> schema?
</>
}
onClick={deleteHandler}
danger
>
Remove schema
</DropdownItem>
</Dropdown>
<ConfirmationModal
isOpen={isDeleteSchemaConfirmationVisible}
onCancel={() => setDeleteSchemaConfirmationVisible(false)}
onConfirm={onDelete}
>
Are you sure want to remove <b>{subject}</b> schema?
</ConfirmationModal>
</>
)}
</PageHeading>

View file

@ -1,5 +1,4 @@
import React from 'react';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import Select from 'components/common/Select/Select';
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
import { useAppDispatch } from 'lib/hooks/redux';
@ -10,6 +9,7 @@ import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
import { ClusterNameRoute } from 'lib/paths';
import { schemasApiClient } from 'lib/api';
import { showServerError } from 'lib/errorHandling';
import { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './GlobalSchemaSelector.styled';
@ -18,17 +18,12 @@ const GlobalSchemaSelector: React.FC = () => {
const dispatch = useAppDispatch();
const [searchText] = useSearch();
const { page, perPage } = usePagination();
const confirm = useConfirm();
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
CompatibilityLevelCompatibilityEnum | undefined
>();
const [isFetching, setIsFetching] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const [isConfirmationVisible, setIsConfirmationVisible] =
React.useState(false);
React.useEffect(() => {
const fetchData = async () => {
@ -49,29 +44,30 @@ const GlobalSchemaSelector: React.FC = () => {
}, [clusterName]);
const handleChangeCompatibilityLevel = (level: string | number) => {
setNextCompatibilityLevel(level as CompatibilityLevelCompatibilityEnum);
setIsConfirmationVisible(true);
};
const handleUpdateCompatibilityLevel = async () => {
setIsUpdating(true);
if (nextCompatibilityLevel) {
try {
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
clusterName,
compatibilityLevel: { compatibility: nextCompatibilityLevel },
});
setCurrentCompatibilityLevel(nextCompatibilityLevel);
setNextCompatibilityLevel(undefined);
setIsConfirmationVisible(false);
dispatch(
fetchSchemas({ clusterName, page, perPage, search: searchText })
);
} catch (e) {
showServerError(e as Response);
const nextLevel = level as CompatibilityLevelCompatibilityEnum;
confirm(
<>
Are you sure you want to update the global compatibility level and set
it to <b>{nextLevel}</b>? This may affect the compatibility levels of
the schemas.
</>,
async () => {
try {
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
clusterName,
compatibilityLevel: {
compatibility: nextLevel,
},
});
setCurrentCompatibilityLevel(nextLevel);
dispatch(
fetchSchemas({ clusterName, page, perPage, search: searchText })
);
} catch (e) {
showServerError(e as Response);
}
}
}
setIsUpdating(false);
);
};
if (!currentCompatibilityLevel) return null;
@ -84,21 +80,11 @@ const GlobalSchemaSelector: React.FC = () => {
defaultValue={currentCompatibilityLevel}
minWidth="200px"
onChange={handleChangeCompatibilityLevel}
disabled={isFetching || isUpdating || isConfirmationVisible}
disabled={isFetching}
options={Object.keys(CompatibilityLevelCompatibilityEnum).map(
(level) => ({ value: level, label: level })
)}
/>
<ConfirmationModal
isOpen={isConfirmationVisible}
onCancel={() => setIsConfirmationVisible(false)}
onConfirm={handleUpdateCompatibilityLevel}
isConfirming={isUpdating}
>
Are you sure you want to update the global compatibility level and set
it to <b>{nextCompatibilityLevel}</b>? This may affect the compatibility
levels of the schemas.
</ConfirmationModal>
</S.Wrapper>
);
};

View file

@ -86,7 +86,7 @@ describe('GlobalSchemaSelector', () => {
200
);
await waitFor(() => {
userEvent.click(screen.getByText('Submit'));
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
});
await waitFor(() => expect(putNewCompatibilityMock.called()).toBeTruthy());
await waitFor(() => expect(getSchemasMock.called()).toBeTruthy());
@ -94,6 +94,9 @@ describe('GlobalSchemaSelector', () => {
await waitFor(() =>
expect(screen.queryByText('Confirm the action')).not.toBeInTheDocument()
);
expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD);
await waitFor(() =>
expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD)
);
});
});

View file

@ -5,13 +5,11 @@ import {
TopicColumnsToSort,
} from 'generated-sources';
import { useAppDispatch } from 'lib/hooks/redux';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { TableCellProps } from 'components/common/SmartTable/TableColumn';
import { TopicWithDetailedInfo } from 'redux/interfaces';
import ClusterContext from 'components/contexts/ClusterContext';
import * as S from 'components/Topics/List/List.styled';
import { ClusterNameRoute } from 'lib/paths';
import useModal from 'lib/hooks/useModal';
import useAppParams from 'lib/hooks/useAppParams';
import {
deleteTopic,
@ -46,85 +44,61 @@ const ActionsCell: React.FC<
const dispatch = useAppDispatch();
const { clusterName } = useAppParams<ClusterNameRoute>();
const {
isOpen: isDeleteTopicModalOpen,
setClose: closeDeleteTopicModal,
setOpen: openDeleteTopicModal,
} = useModal(false);
const {
isOpen: isRecreateTopicModalOpen,
setClose: closeRecreateTopicModal,
setOpen: openRecreateTopicModal,
} = useModal(false);
const {
isOpen: isClearMessagesModalOpen,
setClose: closeClearMessagesModal,
setOpen: openClearMessagesModal,
} = useModal(false);
const isHidden = internal || isReadOnly || !hovered;
const deleteTopicHandler = () => {
dispatch(deleteTopic({ clusterName, topicName: name }));
closeDeleteTopicModal();
};
const clearTopicMessagesHandler = () => {
dispatch(clearTopicMessages({ clusterName, topicName: name }));
dispatch(fetchTopicsList(topicsListParams));
closeClearMessagesModal();
};
const recreateTopicHandler = () => {
dispatch(recreateTopic({ clusterName, topicName: name }));
closeRecreateTopicModal();
};
return (
<>
<S.ActionsContainer>
{!isHidden && (
<Dropdown>
{cleanUpPolicy === CleanUpPolicy.DELETE && (
<DropdownItem onClick={openClearMessagesModal} danger>
Clear Messages
</DropdownItem>
)}
<DropdownItem onClick={openRecreateTopicModal} danger>
Recreate Topic
<S.ActionsContainer>
{!isHidden && (
<Dropdown>
{cleanUpPolicy === CleanUpPolicy.DELETE && (
<DropdownItem
onClick={clearTopicMessagesHandler}
confirm="Are you sure want to clear topic messages?"
danger
>
Clear Messages
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem onClick={openDeleteTopicModal} danger>
Remove Topic
</DropdownItem>
)}
</Dropdown>
)}
</S.ActionsContainer>
<ConfirmationModal
isOpen={isClearMessagesModalOpen}
onCancel={closeClearMessagesModal}
onConfirm={clearTopicMessagesHandler}
>
Are you sure want to clear topic messages?
</ConfirmationModal>
<ConfirmationModal
isOpen={isDeleteTopicModalOpen}
onCancel={closeDeleteTopicModal}
onConfirm={deleteTopicHandler}
>
Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal>
<ConfirmationModal
isOpen={isRecreateTopicModalOpen}
onCancel={closeRecreateTopicModal}
onConfirm={recreateTopicHandler}
>
Are you sure to recreate <b>{name}</b> topic?
</ConfirmationModal>
</>
)}
<DropdownItem
onClick={recreateTopicHandler}
confirm={
<>
Are you sure to recreate <b>{name}</b> topic?
</>
}
danger
>
Recreate Topic
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem
onClick={deleteTopicHandler}
confirm={
<>
Are you sure want to remove <b>{name}</b> topic?
</>
}
danger
>
Remove Topic
</DropdownItem>
)}
</Dropdown>
)}
</S.ActionsContainer>
);
};

View file

@ -14,7 +14,6 @@ import {
import usePagination from 'lib/hooks/usePagination';
import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import {
GetTopicsRequest,
SortOrder,
@ -30,6 +29,7 @@ import { SmartTable } from 'components/common/SmartTable/SmartTable';
import { TableColumn } from 'components/common/SmartTable/TableColumn';
import { useTableState } from 'lib/hooks/useTableState';
import PlusIcon from 'components/common/Icons/PlusIcon';
import { useConfirm } from 'lib/hooks/useConfirm';
import {
MessagesCell,
@ -88,6 +88,7 @@ const List: React.FC<TopicsListProps> = ({
page || null
);
const navigate = useNavigate();
const confirm = useConfirm();
const topicsListParams = React.useMemo(
() => ({
@ -148,16 +149,6 @@ const List: React.FC<TopicsListProps> = ({
});
};
const [confirmationModal, setConfirmationModal] = React.useState<
'' | 'deleteTopics' | 'purgeMessages'
>('');
const [confirmationModalText, setConfirmationModalText] =
React.useState<string>('');
const closeConfirmationModal = () => {
setConfirmationModal('');
};
const clearSelectedTopics = () => tableState.toggleSelection(false);
const searchHandler = (searchString: string) => {
@ -171,16 +162,26 @@ const List: React.FC<TopicsListProps> = ({
search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
});
};
const deleteOrPurgeConfirmationHandler = () => {
const deleteTopicsHandler = () => {
const selectedIds = Array.from(tableState.selectedIds);
if (confirmationModal === 'deleteTopics') {
confirm('Are you sure you want to remove selected topics?', () => {
deleteTopics({ clusterName, topicNames: selectedIds });
} else {
clearTopicsMessages({ clusterName, topicNames: selectedIds });
}
closeConfirmationModal();
clearSelectedTopics();
fetchTopicsList(topicsListParams);
clearSelectedTopics();
fetchTopicsList(topicsListParams);
});
};
const purgeTopicsHandler = () => {
const selectedIds = Array.from(tableState.selectedIds);
confirm(
'Are you sure you want to purge messages of selected topics?',
() => {
clearTopicsMessages({ clusterName, topicNames: selectedIds });
clearSelectedTopics();
fetchTopicsList(topicsListParams);
}
);
};
return (
@ -220,54 +221,35 @@ const List: React.FC<TopicsListProps> = ({
) : (
<div>
{tableState.selectedCount > 0 && (
<>
<ControlPanelWrapper data-testid="delete-buttons">
<Button
buttonSize="M"
buttonType="secondary"
onClick={() => {
setConfirmationModal('deleteTopics');
setConfirmationModalText(
'Are you sure you want to remove selected topics?'
);
}}
>
Delete selected topics
</Button>
{tableState.selectedCount === 1 && (
<Button
buttonSize="M"
buttonType="secondary"
to={{
pathname: clusterTopicCopyRelativePath,
search: `?${getSelectedTopic()}`,
}}
>
Copy selected topic
</Button>
)}
<Button
buttonSize="M"
buttonType="secondary"
onClick={() => {
setConfirmationModal('purgeMessages');
setConfirmationModalText(
'Are you sure you want to purge messages of selected topics?'
);
}}
>
Purge messages of selected topics
</Button>
</ControlPanelWrapper>
<ConfirmationModal
isOpen={confirmationModal !== ''}
onCancel={closeConfirmationModal}
onConfirm={deleteOrPurgeConfirmationHandler}
<ControlPanelWrapper data-testid="delete-buttons">
<Button
buttonSize="M"
buttonType="secondary"
onClick={deleteTopicsHandler}
>
{confirmationModalText}
</ConfirmationModal>
</>
Delete selected topics
</Button>
{tableState.selectedCount === 1 && (
<Button
buttonSize="M"
buttonType="secondary"
to={{
pathname: clusterTopicCopyRelativePath,
search: `?${getSelectedTopic()}`,
}}
>
Copy selected topic
</Button>
)}
<Button
buttonSize="M"
buttonType="secondary"
onClick={purgeTopicsHandler}
>
Purge messages of selected topics
</Button>
</ControlPanelWrapper>
)}
<SmartTable
selectable={!isReadOnly}

View file

@ -280,9 +280,9 @@ describe('List', () => {
)[buttonIndex];
userEvent.click(buttonClickedElement);
const modal = screen.getByRole('dialog');
const modal = await screen.findByRole('dialog');
expect(within(modal).getByText(confirmationText)).toBeInTheDocument();
userEvent.click(within(modal).getByRole('button', { name: 'Submit' }));
userEvent.click(within(modal).getByRole('button', { name: 'Confirm' }));
await waitFor(() => {
expect(screen.queryByTestId('delete-buttons')).not.toBeInTheDocument();

View file

@ -10,7 +10,6 @@ import {
clusterTopicSendMessageRelativePath,
} from 'lib/paths';
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 styled from 'styled-components';
@ -60,42 +59,22 @@ const Details: React.FC<Props> = ({
clearTopicMessages,
}) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const isInternal = useAppSelector((state) =>
getIsTopicInternal(state, topicName)
);
const isDeletePolicy = useAppSelector((state) =>
getIsTopicDeletePolicy(state, topicName)
);
const navigate = useNavigate();
const { isReadOnly, isTopicDeletionAllowed } =
React.useContext(ClusterContext);
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
React.useState(false);
const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
React.useState(false);
const [
isRecreateTopicConfirmationVisible,
setRecreateTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = () => {
deleteTopic({ clusterName, topicName });
setDeleteTopicConfirmationVisible(false);
navigate('../..');
};
const clearTopicMessagesHandler = () => {
clearTopicMessages({ clusterName, topicName });
setClearTopicConfirmationVisible(false);
};
const recreateTopicHandler = () => {
recreateTopic({ clusterName, topicName });
setRecreateTopicConfirmationVisible(false);
};
return (
<div>
<PageHeading text={topicName}>
@ -131,26 +110,44 @@ const Details: React.FC<Props> = ({
especially important consequences.
</DropdownItemHint>
</DropdownItem>
{isDeletePolicy && (
<DropdownItem
disabled={!isDeletePolicy}
onClick={() =>
clearTopicMessages({ clusterName, topicName })
}
confirm="Are you sure want to clear topic messages?"
danger
>
Clear messages
</DropdownItem>
)}
<DropdownItem
disabled={!isDeletePolicy}
onClick={() => setClearTopicConfirmationVisible(true)}
danger
>
Clear messages
</DropdownItem>
<DropdownItem
onClick={() => setRecreateTopicConfirmationVisible(true)}
onClick={() => recreateTopic({ clusterName, topicName })}
confirm={
<>
Are you sure want to recreate <b>{topicName}</b>{' '}
topic?
</>
}
danger
>
Recreate Topic
</DropdownItem>
<DropdownItem
disabled={!isTopicDeletionAllowed}
onClick={() => setDeleteTopicConfirmationVisible(true)}
danger
>
Remove Topic
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem
onClick={deleteTopicHandler}
confirm={
<>
Are you sure want to remove <b>{topicName}</b>{' '}
topic?
</>
}
danger
>
Remove Topic
</DropdownItem>
)}
</Dropdown>
}
/>
@ -158,27 +155,6 @@ const Details: React.FC<Props> = ({
)}
</HeaderControlsWrapper>
</PageHeading>
<ConfirmationModal
isOpen={isDeleteTopicConfirmationVisible}
onCancel={() => setDeleteTopicConfirmationVisible(false)}
onConfirm={deleteTopicHandler}
>
Are you sure want to remove <b>{topicName}</b> topic?
</ConfirmationModal>
<ConfirmationModal
isOpen={isClearTopicConfirmationVisible}
onCancel={() => setClearTopicConfirmationVisible(false)}
onConfirm={clearTopicMessagesHandler}
>
Are you sure want to clear topic messages?
</ConfirmationModal>
<ConfirmationModal
isOpen={isRecreateTopicConfirmationVisible}
onCancel={() => setRecreateTopicConfirmationVisible(false)}
onConfirm={recreateTopicHandler}
>
Are you sure want to recreate <b>{topicName}</b> topic?
</ConfirmationModal>
<Navbar role="navigation">
<NavLink
to="."

View file

@ -298,13 +298,6 @@ export const DeleteSavedFilterIcon = styled.div`
margin-left: 4px;
`;
export const ConfirmDeletionText = styled.h3`
color: ${({ theme }) => theme.modal.deletionTextColor};
font-size: 14px;
line-height: 20px;
padding: 16px 0;
`;
export const MessageLoading = styled.div.attrs({
role: 'contentLoader',
})<MessageLoadingProps>`

View file

@ -1,8 +1,7 @@
import React, { FC } from 'react';
import { Button } from 'components/common/Button/Button';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import useModal from 'lib/hooks/useModal';
import DeleteIcon from 'components/common/Icons/DeleteIcon';
import { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './Filters.styled';
import { MessageFilters } from './Filters';
@ -24,9 +23,8 @@ const SavedFilters: FC<Props> = ({
closeModal,
onGoBack,
}) => {
const { isOpen, setOpen, setClose } = useModal();
const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
const [selectedFilter, setSelectedFilter] = React.useState(-1);
const confirm = useConfirm();
const activeFilter = () => {
if (selectedFilter > -1) {
@ -36,26 +34,13 @@ const SavedFilters: FC<Props> = ({
};
const deleteFilterHandler = (index: number) => {
setOpen();
setDeleteIndex(index);
confirm(<>Are you sure want to remove {filters[index]?.name}?</>, () => {
deleteFilter(index);
});
};
return (
<>
<ConfirmationModal
isOpen={isOpen}
title="Confirm deletion"
onConfirm={() => {
deleteFilter(deleteIndex);
setClose();
}}
onCancel={setClose}
submitBtnText="Delete"
>
<S.ConfirmDeletionText>
Are you sure want to remove {filters[deleteIndex]?.name}?
</S.ConfirmDeletionText>
</ConfirmationModal>
<S.BackToCustomText onClick={onGoBack}>
Back To custom filters
</S.BackToCustomText>
@ -86,7 +71,6 @@ const SavedFilters: FC<Props> = ({
buttonType="secondary"
type="button"
onClick={closeModal}
disabled={isOpen}
>
Cancel
</Button>
@ -95,7 +79,6 @@ const SavedFilters: FC<Props> = ({
buttonType="primary"
type="button"
onClick={activeFilter}
disabled={isOpen}
>
Select filter
</Button>

View file

@ -3,7 +3,12 @@ import SavedFilters, {
Props,
} from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
import { screen, within } from '@testing-library/react';
import {
screen,
waitFor,
waitForElementToBeRemoved,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
@ -11,8 +16,8 @@ jest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');
describe('SavedFilter Component', () => {
const mockFilters: MessageFilters[] = [
{ name: 'name', code: 'code' },
{ name: 'name1', code: 'code1' },
{ name: 'My Filter', code: 'code' },
{ name: 'One More Filter', code: 'code1' },
];
const setUpComponent = (props: Partial<Props> = {}) => {
@ -125,11 +130,11 @@ describe('SavedFilter Component', () => {
const modelDialog = screen.getByRole('dialog');
expect(modelDialog).toBeInTheDocument();
expect(
within(modelDialog).getByText(/Confirm deletion/i)
within(modelDialog).getByText('Are you sure want to remove My Filter?')
).toBeInTheDocument();
});
it('Close Confirmations deletion modal with button', () => {
it('Close Confirmations deletion modal with button', async () => {
setUpComponent({ deleteFilter: deleteMock });
const savedFilters = getSavedFilters();
const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@ -142,11 +147,11 @@ describe('SavedFilter Component', () => {
const cancelButton = within(modelDialog).getByRole('button', {
name: /Cancel/i,
});
userEvent.click(cancelButton);
await waitFor(() => userEvent.click(cancelButton));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('Delete the saved filter', () => {
it('Delete the saved filter', async () => {
setUpComponent({ deleteFilter: deleteMock });
const savedFilters = getSavedFilters();
const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@ -154,9 +159,11 @@ describe('SavedFilter Component', () => {
userEvent.hover(savedFilters[0]);
userEvent.click(deleteIcons[0]);
userEvent.click(screen.getByRole('button', { name: /Delete/i }));
await waitFor(() =>
userEvent.click(screen.getByRole('button', { name: 'Confirm' }))
);
expect(deleteMock).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
});
});

View file

@ -1,5 +1,5 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ClusterContext from 'components/contexts/ClusterContext';
import Details from 'components/Topics/Topic/Details/Details';
@ -97,9 +97,12 @@ describe('Details', () => {
userEvent.click(openModalButton);
});
it('calls deleteTopic on confirm', () => {
const submitButton = screen.getAllByText('Submit')[0];
userEvent.click(submitButton);
it('calls deleteTopic on confirm', async () => {
const submitButton = screen.getAllByRole('button', {
name: 'Confirm',
})[0];
await waitFor(() => userEvent.click(submitButton));
expect(mockDelete).toHaveBeenCalledWith({
clusterName: mockClusterName,
@ -107,10 +110,9 @@ describe('Details', () => {
});
});
it('closes the modal when cancel button is clicked', () => {
it('closes the modal when cancel button is clicked', async () => {
const cancelButton = screen.getAllByText('Cancel')[0];
userEvent.click(cancelButton);
await waitFor(() => userEvent.click(cancelButton));
expect(cancelButton).not.toBeInTheDocument();
});
});
@ -123,9 +125,11 @@ describe('Details', () => {
userEvent.click(confirmButton);
});
it('it calls clearTopicMessages on confirm', () => {
const submitButton = screen.getAllByText('Submit')[0];
userEvent.click(submitButton);
it('it calls clearTopicMessages on confirm', async () => {
const submitButton = screen.getAllByRole('button', {
name: 'Confirm',
})[0];
await waitFor(() => userEvent.click(submitButton));
expect(mockClearTopicMessages).toHaveBeenCalledWith({
clusterName: mockClusterName,
@ -133,9 +137,9 @@ describe('Details', () => {
});
});
it('closes the modal when cancel button is clicked', () => {
it('closes the modal when cancel button is clicked', async () => {
const cancelButton = screen.getAllByText('Cancel')[0];
userEvent.click(cancelButton);
await waitFor(() => userEvent.click(cancelButton));
expect(cancelButton).not.toBeInTheDocument();
});
@ -152,14 +156,14 @@ describe('Details', () => {
});
});
it('redirects to the correct route if topic is deleted', () => {
it('redirects to the correct route if topic is deleted', async () => {
setupComponent();
const deleteTopicButton = screen.getByText(/Remove topic/i);
userEvent.click(deleteTopicButton);
const submitDeleteButton = screen.getByText(/Submit/i);
userEvent.click(submitDeleteButton);
const submitDeleteButton = screen.getByRole('button', { name: 'Confirm' });
await waitFor(() => userEvent.click(submitDeleteButton));
expect(mockNavigate).toHaveBeenCalledWith('../..');
});
@ -184,12 +188,13 @@ describe('Details', () => {
).toBeInTheDocument();
});
it('calling recreation function after click on Submit button', () => {
it('calling recreation function after click on Submit button', async () => {
setupComponent();
const recreateTopicButton = screen.getByText(/Recreate topic/i);
userEvent.click(recreateTopicButton);
const confirmBtn = screen.getByRole('button', { name: /submit/i });
userEvent.click(confirmBtn);
const confirmBtn = screen.getByRole('button', { name: /Confirm/i });
await waitFor(() => userEvent.click(confirmBtn));
expect(mockRecreateTopic).toBeCalledTimes(1);
});

View file

@ -1,6 +1,5 @@
import { ErrorMessage } from '@hookform/error-message';
import { Button } from 'components/common/Button/Button';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import Input from 'components/common/Input/Input';
import { FormError } from 'components/common/Input/Input.styled';
import { InputLabel } from 'components/common/Input/InputLabel.styled';
@ -9,14 +8,13 @@ import { FormProvider, useForm } from 'react-hook-form';
import { RouteParamsClusterTopic } from 'lib/paths';
import { ClusterName, TopicName } from 'redux/interfaces';
import useAppParams from 'lib/hooks/useAppParams';
import { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './DangerZone.styled';
export interface Props {
defaultPartitions: number;
defaultReplicationFactor: number;
partitionsCountIncreased: boolean;
replicationFactorUpdated: boolean;
updateTopicPartitionsCount: (payload: {
clusterName: ClusterName;
topicName: TopicName;
@ -32,19 +30,10 @@ export interface Props {
const DangerZone: React.FC<Props> = ({
defaultPartitions,
defaultReplicationFactor,
partitionsCountIncreased,
replicationFactorUpdated,
updateTopicPartitionsCount,
updateTopicReplicationFactor,
}) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
React.useState<boolean>(false);
const [
isReplicationFactorConfirmationVisible,
setIsReplicationFactorConfirmationVisible,
] = React.useState<boolean>(false);
const [partitions, setPartitions] = React.useState<number>(defaultPartitions);
const [replicationFactor, setReplicationFactor] = React.useState<number>(
defaultReplicationFactor
@ -62,6 +51,29 @@ const DangerZone: React.FC<Props> = ({
},
});
const confirm = useConfirm();
const confirmPartitionsChange = () =>
confirm(
`Are you sure you want to increase the number of partitions?
Do it only if you 100% know what you are doing!`,
() =>
updateTopicPartitionsCount({
clusterName,
topicName,
partitions: partitionsMethods.getValues('partitions'),
})
);
const confirmReplicationFactorChange = () =>
confirm('Are you sure you want to update the replication factor?', () =>
updateTopicReplicationFactor({
clusterName,
topicName,
replicationFactor:
replicationFactorMethods.getValues('replicationFactor'),
})
);
const validatePartitions = (data: { partitions: number }) => {
if (data.partitions < defaultPartitions) {
partitionsMethods.setError('partitions', {
@ -70,42 +82,15 @@ const DangerZone: React.FC<Props> = ({
});
} else {
setPartitions(data.partitions);
setIsPartitionsConfirmationVisible(true);
confirmPartitionsChange();
}
};
const validateReplicationFactor = (data: { replicationFactor: number }) => {
setReplicationFactor(data.replicationFactor);
setIsReplicationFactorConfirmationVisible(true);
confirmReplicationFactorChange();
};
React.useEffect(() => {
if (partitionsCountIncreased) {
setIsPartitionsConfirmationVisible(false);
}
}, [partitionsCountIncreased]);
React.useEffect(() => {
if (replicationFactorUpdated) {
setIsReplicationFactorConfirmationVisible(false);
}
}, [replicationFactorUpdated]);
const partitionsSubmit = () => {
updateTopicPartitionsCount({
clusterName,
topicName,
partitions: partitionsMethods.getValues('partitions'),
});
};
const replicationFactorSubmit = () => {
updateTopicReplicationFactor({
clusterName,
topicName,
replicationFactor:
replicationFactorMethods.getValues('replicationFactor'),
});
};
return (
<S.Wrapper>
<S.Title>Danger Zone</S.Title>
@ -148,15 +133,6 @@ const DangerZone: React.FC<Props> = ({
name="partitions"
/>
</FormError>
<ConfirmationModal
isOpen={isPartitionsConfirmationVisible}
onCancel={() => setIsPartitionsConfirmationVisible(false)}
onConfirm={partitionsSubmit}
>
Are you sure you want to increase the number of partitions? Do it only
if you 100% know what you are doing!
</ConfirmationModal>
<FormProvider {...replicationFactorMethods}>
<S.Form
onSubmit={replicationFactorMethods.handleSubmit(
@ -170,6 +146,7 @@ const DangerZone: React.FC<Props> = ({
</InputLabel>
<Input
id="replicationFactor"
inputSize="M"
type="number"
placeholder="Replication Factor"
name="replicationFactor"
@ -190,20 +167,12 @@ const DangerZone: React.FC<Props> = ({
</div>
</S.Form>
</FormProvider>
<FormError>
<ErrorMessage
errors={replicationFactorMethods.formState.errors}
name="replicationFactor"
/>
</FormError>
<ConfirmationModal
isOpen={isReplicationFactorConfirmationVisible}
onCancel={() => setIsReplicationFactorConfirmationVisible(false)}
onConfirm={replicationFactorSubmit}
>
Are you sure you want to update the replication factor?
</ConfirmationModal>
</div>
</S.Wrapper>
);

View file

@ -4,10 +4,6 @@ import {
updateTopicPartitionsCount,
updateTopicReplicationFactor,
} from 'redux/reducers/topics/topicsSlice';
import {
getTopicPartitionsCountIncreased,
getTopicReplicationFactorUpdated,
} from 'redux/reducers/topics/selectors';
import DangerZone from './DangerZone';
@ -17,13 +13,11 @@ type OwnProps = {
};
const mapStateToProps = (
state: RootState,
_: RootState,
{ defaultPartitions, defaultReplicationFactor }: OwnProps
) => ({
defaultPartitions,
defaultReplicationFactor,
partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
replicationFactorUpdated: getTopicReplicationFactorUpdated(state),
});
const mapDispatchToProps = {

View file

@ -20,8 +20,6 @@ const renderComponent = (props?: Partial<Props>) =>
<DangerZone
defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased={false}
replicationFactorUpdated={false}
updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()}
{...props}
@ -33,7 +31,7 @@ const renderComponent = (props?: Partial<Props>) =>
const clickOnDialogSubmitButton = () => {
userEvent.click(
within(screen.getByRole('dialog')).getByRole('button', {
name: 'Submit',
name: 'Confirm',
})
);
};
@ -41,7 +39,7 @@ const clickOnDialogSubmitButton = () => {
const checkDialogThenPressCancel = async () => {
const dialog = screen.getByRole('dialog');
expect(screen.getByRole('dialog')).toBeInTheDocument();
userEvent.click(within(dialog).getByText(/cancel/i));
userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
);
@ -174,73 +172,6 @@ describe('DangerZone', () => {
);
});
it('should close any popup if the partitionsCount is Increased ', async () => {
renderComponent({ partitionsCountIncreased: true });
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
);
});
it('should close any popup if the replicationFactor is Updated', async () => {
renderComponent({ replicationFactorUpdated: true });
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
);
});
it('should already opened Confirmation popup if partitionsCount is Increased', async () => {
const { rerender } = renderComponent();
const partitionInput = screen.getByPlaceholderText('Number of partitions');
const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0];
await waitFor(() => {
userEvent.type(partitionInput, '5');
});
userEvent.click(partitionInputSubmitBtn);
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
rerender(
<DangerZone
defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased
replicationFactorUpdated={false}
updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()}
/>
);
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
);
});
it('should already opened Confirmation popup if replicationFactor is Increased', async () => {
const { rerender } = renderComponent();
const replicatorFactorInput =
screen.getByPlaceholderText('Replication Factor');
const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1];
await waitFor(() => {
userEvent.type(replicatorFactorInput, '5');
});
userEvent.click(replicatorFactorInputSubmitBtn);
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
rerender(
<DangerZone
defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased={false}
replicationFactorUpdated
updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()}
/>
);
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
);
});
it('should close the partitions dialog if he cancel button is pressed', async () => {
renderComponent();

View file

@ -15,7 +15,7 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));
const renderComponent = (
const renderComponent = async (
props: Partial<Props> = {},
topic: TopicWithDetailedInfo | null = topicWithInfo
) => {
@ -27,7 +27,7 @@ const renderComponent = (
topics = getTopicStateFixtures([topic]);
}
return render(
render(
<WithRoute path={clusterTopicEditPath()}>
<Edit
isFetched
@ -47,8 +47,8 @@ const renderComponent = (
describe('Edit Component', () => {
afterEach(() => {});
it('renders the Edit Component', () => {
renderComponent();
it('renders the Edit Component', async () => {
await act(() => renderComponent());
expect(
screen.getByRole('heading', { name: `Edit ${topicName}` })
@ -58,8 +58,10 @@ describe('Edit Component', () => {
).toBeInTheDocument();
});
it('should check Edit component renders null is not rendered when topic is not passed', () => {
renderComponent({}, { ...topicWithInfo, config: undefined });
it('should check Edit component renders null is not rendered when topic is not passed', async () => {
await act(() =>
renderComponent({}, { ...topicWithInfo, config: undefined })
);
expect(
screen.queryByRole('heading', { name: `Edit ${topicName}` })
).not.toBeInTheDocument();
@ -68,8 +70,8 @@ describe('Edit Component', () => {
).not.toBeInTheDocument();
});
it('should check Edit component renders null is not isFetched is false', () => {
renderComponent({ isFetched: false });
it('should check Edit component renders null is not isFetched is false', async () => {
await act(() => renderComponent({ isFetched: false }));
expect(
screen.queryByRole('heading', { name: `Edit ${topicName}` })
).not.toBeInTheDocument();
@ -78,10 +80,10 @@ describe('Edit Component', () => {
).not.toBeInTheDocument();
});
it('should check Edit component renders null is not topic config is not passed is false', () => {
it('should check Edit component renders null is not topic config is not passed is false', async () => {
const modifiedTopic = { ...topicWithInfo };
modifiedTopic.config = undefined;
renderComponent({}, modifiedTopic);
await act(() => renderComponent({}, modifiedTopic));
expect(
screen.queryByRole('heading', { name: `Edit ${topicName}` })
).not.toBeInTheDocument();
@ -92,7 +94,9 @@ describe('Edit Component', () => {
describe('Edit Component with its topic default and modified values', () => {
it('should check the default partitions value in the DangerZone', async () => {
renderComponent({}, { ...topicWithInfo, partitionCount: 0 });
await act(() =>
renderComponent({}, { ...topicWithInfo, partitionCount: 0 })
);
// cause topic selector will return falsy
expect(
screen.queryByRole('heading', { name: `Edit ${topicName}` })
@ -103,7 +107,9 @@ describe('Edit Component', () => {
});
it('should check the default partitions value in the DangerZone', async () => {
renderComponent({}, { ...topicWithInfo, replicationFactor: undefined });
await act(() =>
renderComponent({}, { ...topicWithInfo, replicationFactor: undefined })
);
expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue(
DEFAULTS.replicationFactor
);
@ -114,7 +120,9 @@ describe('Edit Component', () => {
it('should check the submit functionality when topic updated is false', async () => {
const updateTopicMock = jest.fn();
renderComponent({ updateTopic: updateTopicMock }, undefined);
await act(() =>
renderComponent({ updateTopic: updateTopicMock }, undefined)
);
const btn = screen.getAllByText(/Save/i)[0];
@ -134,10 +142,11 @@ describe('Edit Component', () => {
it('should check the submit functionality when topic updated is true', async () => {
const updateTopicMock = jest.fn();
renderComponent(
{ updateTopic: updateTopicMock, isTopicUpdated: true },
undefined
await act(() =>
renderComponent(
{ updateTopic: updateTopicMock, isTopicUpdated: true },
undefined
)
);
const btn = screen.getAllByText(/Save/i)[0];

View file

@ -1,7 +1,7 @@
import React from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
import { TopicName } from 'redux/interfaces';
import { ClusterName, TopicName } from 'redux/interfaces';
import { ErrorMessage } from '@hookform/error-message';
import Select, { SelectOption } from 'components/common/Select/Select';
import Input from 'components/common/Input/Input';
@ -9,6 +9,9 @@ import { Button } from 'components/common/Button/Button';
import { InputLabel } from 'components/common/Input/InputLabel.styled';
import { FormError } from 'components/common/Input/Input.styled';
import { StyledForm } from 'components/common/Form/Form.styled';
import { clusterTopicsPath } from 'lib/paths';
import { useNavigate } from 'react-router-dom';
import useAppParams from 'lib/hooks/useAppParams';
import CustomParams from './CustomParams/CustomParams';
import TimeToRetain from './TimeToRetain';
@ -54,7 +57,10 @@ const TopicForm: React.FC<Props> = ({
const {
control,
formState: { errors, isDirty, isValid },
reset,
} = useFormContext();
const navigate = useNavigate();
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
const getCleanUpPolicy =
CleanupPolicyOptions.find((option: SelectOption) => {
return (
@ -68,6 +74,11 @@ const TopicForm: React.FC<Props> = ({
return option.value === retentionBytes;
})?.value || RetentionBytesOptions[0].value;
const onCancel = () => {
reset();
navigate(clusterTopicsPath(clusterName));
};
return (
<StyledForm onSubmit={onSubmit} aria-label="topic form">
<fieldset disabled={isSubmitting}>
@ -228,7 +239,12 @@ const TopicForm: React.FC<Props> = ({
>
{isEditing ? 'Save' : 'Create topic'}
</Button>
<Button type="button" buttonType="primary" buttonSize="L">
<Button
type="button"
buttonType="primary"
buttonSize="L"
onClick={onCancel}
>
Cancel
</Button>
</S.ButtonWrapper>

View file

@ -4,6 +4,7 @@ import styled from 'styled-components';
export const Alert = styled.div<{ $type: ToastType }>`
background-color: ${({ $type, theme }) => theme.alert.color[$type]};
min-width: 400px;
max-width: 600px;
min-height: 64px;
border-radius: 8px;
padding: 12px;

View file

@ -1,59 +1,64 @@
import styled, { css } from 'styled-components';
export const ConfirmationModalWrapper = styled.div.attrs({ role: 'dialog' })(
({ theme }) => css`
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
overflow: hidden;
position: fixed;
z-index: 40;
export const Wrapper = styled.div.attrs({ role: 'dialog' })`
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
overflow: hidden;
position: fixed;
z-index: 40;
bottom: 0;
left: 0;
right: 0;
top: 0;
`;
export const Overlay = styled.div(
({ theme: { modal } }) => css`
background-color: ${modal.overlay};
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
& div:first-child {
background-color: ${theme.modal.overlay};
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
& div:last-child {
position: absolute;
display: flex;
flex-direction: column;
width: 560px;
border-radius: 8px;
background-color: ${theme.modal.backgroundColor};
filter: drop-shadow(0px 4px 16px ${theme.modal.shadow});
& > * {
padding: 16px;
width: 100%;
}
& > header {
height: 64px;
font-size: 20px;
text-align: start;
}
& > section {
border-top: 1px solid ${theme.modal.border.top};
border-bottom: 1px solid ${theme.modal.border.bottom};
}
& > footer {
height: 64px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
`
);
export const Modal = styled.div(
({ theme: { modal } }) => css`
position: absolute;
display: flex;
flex-direction: column;
width: 560px;
border-radius: 8px;
background-color: ${modal.backgroundColor};
filter: drop-shadow(0px 4px 16px ${modal.shadow});
`
);
export const Header = styled.div`
font-size: 20px;
text-align: start;
padding: 16px;
width: 100%;
`;
export const Content = styled.div(
({ theme: { modal } }) => css`
padding: 16px;
width: 100%;
border-top: 1px solid ${modal.border.top};
border-bottom: 1px solid ${modal.border.bottom};
`
);
export const Footer = styled.div`
height: 64px;
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 16px;
width: 100%;
`;

View file

@ -1,64 +1,42 @@
import React, { PropsWithChildren } from 'react';
import React from 'react';
import { Button } from 'components/common/Button/Button';
import { ConfirmContext } from 'components/contexts/ConfirmContext';
import { ConfirmationModalWrapper } from './ConfirmationModal.styled';
import * as S from './ConfirmationModal.styled';
export interface ConfirmationModalProps {
isOpen?: boolean;
title?: React.ReactNode;
onConfirm(): void;
onCancel(): void;
isConfirming?: boolean;
submitBtnText?: string;
}
const ConfirmationModal: React.FC = () => {
const context = React.useContext(ConfirmContext);
const isOpen = context?.content && context?.confirm;
const ConfirmationModal: React.FC<
PropsWithChildren<ConfirmationModalProps>
> = ({
isOpen,
children,
title = 'Confirm the action',
onCancel,
onConfirm,
isConfirming = false,
submitBtnText = 'Submit',
}) => {
const cancelHandler = () => {
if (!isConfirming) onCancel();
};
if (!isOpen) return null;
return isOpen ? (
<ConfirmationModalWrapper>
<div onClick={cancelHandler} aria-hidden="true" role="button" />
<div>
<header>
<p>{title}</p>
</header>
<section>{children}</section>
<footer>
return (
<S.Wrapper role="dialog" aria-label="Confirmation Dialog">
<S.Overlay onClick={context.cancel} aria-hidden="true" role="button" />
<S.Modal>
<S.Header>Confirm the action</S.Header>
<S.Content>{context.content}</S.Content>
<S.Footer>
<Button
buttonType="secondary"
buttonSize="M"
onClick={cancelHandler}
onClick={context.cancel}
type="button"
disabled={isConfirming}
>
Cancel
</Button>
<Button
buttonType="primary"
buttonSize="M"
onClick={onConfirm}
onClick={context.confirm}
type="button"
disabled={isConfirming}
>
{submitBtnText}
Confirm
</Button>
</footer>
</div>
</ConfirmationModalWrapper>
) : null;
</S.Footer>
</S.Modal>
</S.Wrapper>
);
};
export default ConfirmationModal;

View file

@ -1,101 +0,0 @@
import React from 'react';
import ConfirmationModal, {
ConfirmationModalProps,
} from 'components/common/ConfirmationModal/ConfirmationModal';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
const confirmMock = jest.fn();
const cancelMock = jest.fn();
const body = 'Please Confirm the action!';
describe('ConfirmationModal', () => {
const setupWrapper = (props: Partial<ConfirmationModalProps> = {}) => (
<ConfirmationModal onCancel={cancelMock} onConfirm={confirmMock} {...props}>
{body}
</ConfirmationModal>
);
it('renders nothing', () => {
render(setupWrapper({ isOpen: false }));
expect(screen.queryByText(body)).not.toBeInTheDocument();
});
it('renders modal', () => {
render(setupWrapper({ isOpen: true }));
expect(screen.getByRole('dialog')).toHaveTextContent(body);
expect(screen.getAllByRole('button').length).toEqual(2);
});
it('renders modal with default header', () => {
render(setupWrapper({ isOpen: true }));
expect(screen.getByText('Confirm the action')).toBeInTheDocument();
});
it('renders modal with custom header', () => {
const title = 'My Custom Header';
render(setupWrapper({ isOpen: true, title }));
expect(screen.getByText(title)).toBeInTheDocument();
});
it('Check the text on the submit button default behavior', () => {
render(setupWrapper({ isOpen: true }));
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
it('handles onConfirm when user clicks confirm button', () => {
render(setupWrapper({ isOpen: true }));
const confirmBtn = screen.getByRole('button', { name: 'Submit' });
userEvent.click(confirmBtn);
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(1);
});
it('Check the text on the submit button', () => {
const submitBtnText = 'Submit btn Text';
render(setupWrapper({ isOpen: true, submitBtnText }));
expect(
screen.getByRole('button', { name: submitBtnText })
).toBeInTheDocument();
});
describe('cancellation', () => {
describe('when not confirming', () => {
beforeEach(() => {
render(setupWrapper({ isOpen: true }));
});
it('handles onCancel when user clicks on modal-background', () => {
const { container } = render(setupWrapper({ isOpen: true }));
userEvent.click(container.children[0].children[0]);
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('handles onCancel when user clicks on Cancel button', () => {
const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
userEvent.click(cancelBtn);
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
});
describe('when confirming', () => {
beforeEach(() => {
render(setupWrapper({ isOpen: true, isConfirming: true }));
});
it('does not call onCancel when user clicks on modal-background', () => {
userEvent.click(screen.getByRole('dialog'));
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('does not call onCancel when user clicks on Cancel button', () => {
const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
userEvent.click(cancelBtn);
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
});
});
});

View file

@ -60,6 +60,7 @@ export const DropdownButton = styled.button`
border: none;
display: flex;
cursor: pointer;
align-self: center;
`;
export const DangerItem = styled.div`

View file

@ -1,26 +1,36 @@
import React, { PropsWithChildren } from 'react';
import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
import { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './Dropdown.styled';
interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
danger?: boolean;
onClick?(): void;
confirm?: React.ReactNode;
}
const DropdownItem: React.FC<DropdownItemProps> = ({
onClick,
danger,
children,
confirm,
...rest
}) => {
const confirmation = useConfirm();
const handleClick = (e: ClickEvent) => {
if (!onClick) return;
// eslint-disable-next-line no-param-reassign
e.stopPropagation = true;
e.syntheticEvent.stopPropagation();
onClick();
if (confirm) {
confirmation(confirm, onClick);
} else {
onClick();
}
};
return (

View file

@ -1,61 +0,0 @@
import React from 'react';
import { render } from 'lib/testHelpers';
import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
import { ISelectProps } from 'react-multi-select-component/dist/lib/interfaces';
const Option1 = { value: 1, label: 'option 1' };
const Option2 = { value: 2, label: 'option 2' };
interface IMultiSelectProps extends ISelectProps {
minWidth?: string;
}
const DefaultProps: IMultiSelectProps = {
options: [Option1, Option2],
labelledBy: 'multi-select',
value: [Option1, Option2],
};
describe('MultiSelect.Styled', () => {
const setUpComponent = (props: IMultiSelectProps = DefaultProps) => {
const { container } = render(<MultiSelect {...props} />);
const multiSelect = container.firstChild;
const dropdownContainer = multiSelect?.firstChild?.firstChild;
return { container, multiSelect, dropdownContainer };
};
it('should have 200px minWidth by default', () => {
const { container } = setUpComponent();
const multiSelect = container.firstChild;
expect(multiSelect).toHaveStyle('min-width: 200px');
});
it('should have the provided minWidth in styles', () => {
const minWidth = '400px';
const { container } = setUpComponent({ ...DefaultProps, minWidth });
const multiSelect = container.firstChild;
expect(multiSelect).toHaveStyle(`min-width: ${minWidth}`);
});
describe('when not disabled', () => {
it('should have cursor pointer', () => {
const { dropdownContainer } = setUpComponent();
expect(dropdownContainer).toHaveStyle(`cursor: pointer`);
});
});
describe('when disabled', () => {
it('should have cursor not-allowed', () => {
const { dropdownContainer } = setUpComponent({
...DefaultProps,
disabled: true,
});
expect(dropdownContainer).toHaveStyle(`cursor: not-allowed`);
});
});
});

View file

@ -0,0 +1,39 @@
import React, { useState } from 'react';
type ConfirmContextType = {
content: React.ReactNode;
confirm?: () => void;
setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;
setConfirm: React.Dispatch<React.SetStateAction<(() => void) | undefined>>;
cancel: () => void;
};
export const ConfirmContext = React.createContext<ConfirmContextType | null>(
null
);
export const ConfirmContextProvider: React.FC<
React.PropsWithChildren<unknown>
> = ({ children }) => {
const [content, setContent] = useState<React.ReactNode>(null);
const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined);
const cancel = () => {
setContent(null);
setConfirm(undefined);
};
return (
<ConfirmContext.Provider
value={{
content,
setContent,
confirm,
setConfirm,
cancel,
}}
>
{children}
</ConfirmContext.Provider>
);
};

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { render } from 'lib/testHelpers';
import useDataSaver from 'lib/hooks/useDataSaver';
import { render } from '@testing-library/react';
describe('useDataSaver hook', () => {
const content = {

View file

@ -0,0 +1,14 @@
import { ConfirmContext } from 'components/contexts/ConfirmContext';
import React, { useContext } from 'react';
export const useConfirm = () => {
const context = useContext(ConfirmContext);
return (message: React.ReactNode, callback: () => void | Promise<void>) => {
context?.setContent(message);
context?.setConfirm(() => async () => {
await callback();
context?.cancel();
});
};
};

View file

@ -18,6 +18,8 @@ import {
QueryClientProvider,
UseQueryResult,
} from '@tanstack/react-query';
import { ConfirmContextProvider } from 'components/contexts/ConfirmContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
preloadedState?: Partial<RootState>;
@ -67,13 +69,18 @@ const customRender = (
children,
}) => (
<ThemeProvider theme={theme}>
<Provider store={store}>
<TestQueryClientProvider>
<MemoryRouter initialEntries={initialEntries}>
{children}
</MemoryRouter>
</TestQueryClientProvider>
</Provider>
<ConfirmContextProvider>
<Provider store={store}>
<TestQueryClientProvider>
<MemoryRouter initialEntries={initialEntries}>
<div>
{children}
<ConfirmationModal />
</div>
</MemoryRouter>
</TestQueryClientProvider>
</Provider>
</ConfirmContextProvider>
</ThemeProvider>
);
return render(ui, { wrapper: AllTheProviders, ...renderOptions });

View file

@ -27,7 +27,7 @@ export const clearTopicMessages = createAsyncThunk<
dispatch(fetchTopicDetails({ clusterName, topicName }));
showSuccessAlert({
id: `message-${topicName}-${clusterName}-${partitions}`,
message: 'Messages successfully cleared!',
message: `${topicName} messages have been successfully cleared!`,
});
return undefined;

View file

@ -20,12 +20,6 @@ describe('Topics selectors', () => {
expect(selectors.getTopicMessageSchemaFetched(store.getState())).toEqual(
false
);
expect(
selectors.getTopicPartitionsCountIncreased(store.getState())
).toEqual(false);
expect(
selectors.getTopicReplicationFactorUpdated(store.getState())
).toEqual(false);
expect(
selectors.getTopicsConsumerGroupsFetched(store.getState())
).toEqual(false);

View file

@ -11,8 +11,6 @@ import {
fetchTopicConsumerGroups,
createTopic,
deleteTopic,
updateTopicPartitionsCount,
updateTopicReplicationFactor,
} from 'redux/reducers/topics/topicsSlice';
import { AsyncRequestStatus } from 'lib/constants';
@ -90,24 +88,6 @@ export const getTopicMessageSchemaFetched = createSelector(
(status) => status === AsyncRequestStatus.fulfilled
);
const getPartitionsCountIncreaseStatus = createFetchingSelector(
updateTopicPartitionsCount.typePrefix
);
export const getTopicPartitionsCountIncreased = createSelector(
getPartitionsCountIncreaseStatus,
(status) => status === AsyncRequestStatus.fulfilled
);
const getReplicationFactorUpdateStatus = createFetchingSelector(
updateTopicReplicationFactor.typePrefix
);
export const getTopicReplicationFactorUpdated = createSelector(
getReplicationFactorUpdateStatus,
(status) => status === AsyncRequestStatus.fulfilled
);
const getTopicConsumerGroupsStatus = createFetchingSelector(
fetchTopicConsumerGroups.typePrefix
);

View file

@ -274,7 +274,7 @@ export const updateTopicPartitionsCount = createAsyncThunk<
});
showSuccessAlert({
id: `message-${topicName}-${clusterName}-${partitions}`,
message: 'Number of partitions successfully increased!',
message: 'Number of partitions successfully increased',
});
dispatch(fetchTopicDetails({ clusterName, topicName }));
return undefined;
@ -294,7 +294,7 @@ export const updateTopicReplicationFactor = createAsyncThunk<
}
>(
'topic/updateTopicReplicationFactor',
async (payload, { rejectWithValue }) => {
async (payload, { rejectWithValue, dispatch }) => {
try {
const { clusterName, topicName, replicationFactor } = payload;
@ -303,7 +303,11 @@ export const updateTopicReplicationFactor = createAsyncThunk<
topicName,
replicationFactorChange: { totalReplicationFactor: replicationFactor },
});
showSuccessAlert({
id: `message-${topicName}-${clusterName}-replicationFactor`,
message: 'Replication Factor successfully updated',
});
dispatch(fetchTopicDetails({ clusterName, topicName }));
return undefined;
} catch (err) {
showServerError(err as Response);
@ -340,22 +344,24 @@ export const clearTopicsMessages = createAsyncThunk<
clusterName: ClusterName;
topicNames: TopicName[];
}
>('topic/clearTopicsMessages', async (payload, { rejectWithValue }) => {
try {
const { clusterName, topicNames } = payload;
>(
'topic/clearTopicsMessages',
async (payload, { rejectWithValue, dispatch }) => {
try {
const { clusterName, topicNames } = payload;
topicNames.forEach((topicName) => {
dispatch(clearTopicMessages({ clusterName, topicName }));
});
topicNames.forEach((topicName) => {
clearTopicMessages({ clusterName, topicName });
});
return undefined;
} catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
return undefined;
} catch (err) {
showServerError(err as Response);
return rejectWithValue(await getResponse(err as Response));
}
}
});
);
export const initialState: TopicsState = {
const initialState: TopicsState = {
byName: {},
allNames: [],
totalPages: 1,