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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,9 @@
import React from 'react'; import React from 'react';
import { connectors } from 'lib/fixtures/kafkaConnect'; import { connectors } from 'lib/fixtures/kafkaConnect';
import ListItem, { ListItemProps } from 'components/Connect/List/ListItem'; import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
describe('Connectors ListItem', () => { describe('Connectors ListItem', () => {
const connector = connectors[0]; const connector = connectors[0];
const setupWrapper = (props: Partial<ListItemProps> = {}) => ( const setupWrapper = (props: Partial<ListItemProps> = {}) => (
@ -21,25 +14,6 @@ describe('Connectors ListItem', () => {
</table> </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', () => { it('renders item', () => {
render(setupWrapper()); render(setupWrapper());
expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2'); expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2');
@ -76,22 +50,4 @@ describe('Connectors ListItem', () => {
); );
expect(screen.getAllByRole('cell')[6]).toHaveTextContent(''); 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, ClusterGroupParam,
} from 'lib/paths'; } from 'lib/paths';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
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 * as Metrics from 'components/common/Metrics'; import * as Metrics from 'components/common/Metrics';
@ -38,17 +37,14 @@ const Details: React.FC = () => {
const isDeleted = useAppSelector(getIsConsumerGroupDeleted); const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled); const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
React.useState<boolean>(false);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID })); dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
}, [clusterName, consumerGroupID, dispatch]); }, [clusterName, consumerGroupID, dispatch]);
const onDelete = () => { const onDelete = () => {
setIsConfirmationModalVisible(false);
dispatch(deleteConsumerGroup({ clusterName, consumerGroupID })); dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
}; };
React.useEffect(() => { React.useEffect(() => {
if (isDeleted) { if (isDeleted) {
navigate('../'); navigate('../');
@ -73,7 +69,8 @@ const Details: React.FC = () => {
<Dropdown> <Dropdown>
<DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem> <DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
<DropdownItem <DropdownItem
onClick={() => setIsConfirmationModalVisible(true)} confirm="Are you sure you want to delete this consumer group?"
onClick={onDelete}
danger danger
> >
Delete consumer group Delete consumer group
@ -119,13 +116,6 @@ const Details: React.FC = () => {
))} ))}
</tbody> </tbody>
</Table> </Table>
<ConfirmationModal
isOpen={isConfirmationModalVisible}
onCancel={() => setIsConfirmationModalVisible(false)}
onConfirm={onDelete}
>
Are you sure you want to delete this consumer group?
</ConfirmationModal>
</div> </div>
); );
}; };

View file

@ -97,12 +97,13 @@ describe('Details component', () => {
`/api/clusters/${clusterName}/consumer-groups/${groupId}`, `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
200 200
); );
await act(() => { await waitFor(() => {
userEvent.click(screen.getByText('Submit')); userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
}); });
expect(deleteConsumerGroupMock.called()).toBeTruthy(); 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, clusterSchemaSchemaComparePageRelativePath,
} from 'lib/paths'; } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
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';
@ -38,10 +37,6 @@ const Details: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName, subject } = useAppParams<ClusterSubjectParam>(); const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
const [
isDeleteSchemaConfirmationVisible,
setDeleteSchemaConfirmationVisible,
] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchLatestSchema({ clusterName, subject })); dispatch(fetchLatestSchema({ clusterName, subject }));
@ -62,7 +57,7 @@ const Details: React.FC = () => {
const isFetched = useAppSelector(getAreSchemaLatestFulfilled); const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled); const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
const onDelete = async () => { const deleteHandler = async () => {
try { try {
await schemasApiClient.deleteSchema({ await schemasApiClient.deleteSchema({
clusterName, clusterName,
@ -101,19 +96,17 @@ const Details: React.FC = () => {
</Button> </Button>
<Dropdown> <Dropdown>
<DropdownItem <DropdownItem
onClick={() => setDeleteSchemaConfirmationVisible(true)} confirm={
<>
Are you sure want to remove <b>{subject}</b> schema?
</>
}
onClick={deleteHandler}
danger danger
> >
Remove schema Remove schema
</DropdownItem> </DropdownItem>
</Dropdown> </Dropdown>
<ConfirmationModal
isOpen={isDeleteSchemaConfirmationVisible}
onCancel={() => setDeleteSchemaConfirmationVisible(false)}
onConfirm={onDelete}
>
Are you sure want to remove <b>{subject}</b> schema?
</ConfirmationModal>
</> </>
)} )}
</PageHeading> </PageHeading>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -298,13 +298,6 @@ export const DeleteSavedFilterIcon = styled.div`
margin-left: 4px; 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({ export const MessageLoading = styled.div.attrs({
role: 'contentLoader', role: 'contentLoader',
})<MessageLoadingProps>` })<MessageLoadingProps>`

View file

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

View file

@ -3,7 +3,12 @@ import SavedFilters, {
Props, Props,
} from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters'; } from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters'; 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 userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
@ -11,8 +16,8 @@ jest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');
describe('SavedFilter Component', () => { describe('SavedFilter Component', () => {
const mockFilters: MessageFilters[] = [ const mockFilters: MessageFilters[] = [
{ name: 'name', code: 'code' }, { name: 'My Filter', code: 'code' },
{ name: 'name1', code: 'code1' }, { name: 'One More Filter', code: 'code1' },
]; ];
const setUpComponent = (props: Partial<Props> = {}) => { const setUpComponent = (props: Partial<Props> = {}) => {
@ -125,11 +130,11 @@ describe('SavedFilter Component', () => {
const modelDialog = screen.getByRole('dialog'); const modelDialog = screen.getByRole('dialog');
expect(modelDialog).toBeInTheDocument(); expect(modelDialog).toBeInTheDocument();
expect( expect(
within(modelDialog).getByText(/Confirm deletion/i) within(modelDialog).getByText('Are you sure want to remove My Filter?')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('Close Confirmations deletion modal with button', () => { it('Close Confirmations deletion modal with button', async () => {
setUpComponent({ deleteFilter: deleteMock }); setUpComponent({ deleteFilter: deleteMock });
const savedFilters = getSavedFilters(); const savedFilters = getSavedFilters();
const deleteIcons = screen.getAllByText('mock-DeleteIcon'); const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@ -142,11 +147,11 @@ describe('SavedFilter Component', () => {
const cancelButton = within(modelDialog).getByRole('button', { const cancelButton = within(modelDialog).getByRole('button', {
name: /Cancel/i, name: /Cancel/i,
}); });
userEvent.click(cancelButton); await waitFor(() => userEvent.click(cancelButton));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
}); });
it('Delete the saved filter', () => { it('Delete the saved filter', async () => {
setUpComponent({ deleteFilter: deleteMock }); setUpComponent({ deleteFilter: deleteMock });
const savedFilters = getSavedFilters(); const savedFilters = getSavedFilters();
const deleteIcons = screen.getAllByText('mock-DeleteIcon'); const deleteIcons = screen.getAllByText('mock-DeleteIcon');
@ -154,9 +159,11 @@ describe('SavedFilter Component', () => {
userEvent.hover(savedFilters[0]); userEvent.hover(savedFilters[0]);
userEvent.click(deleteIcons[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(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 React from 'react';
import { screen } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import Details from 'components/Topics/Topic/Details/Details'; import Details from 'components/Topics/Topic/Details/Details';
@ -97,9 +97,12 @@ describe('Details', () => {
userEvent.click(openModalButton); userEvent.click(openModalButton);
}); });
it('calls deleteTopic on confirm', () => { it('calls deleteTopic on confirm', async () => {
const submitButton = screen.getAllByText('Submit')[0]; const submitButton = screen.getAllByRole('button', {
userEvent.click(submitButton); name: 'Confirm',
})[0];
await waitFor(() => userEvent.click(submitButton));
expect(mockDelete).toHaveBeenCalledWith({ expect(mockDelete).toHaveBeenCalledWith({
clusterName: mockClusterName, 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]; const cancelButton = screen.getAllByText('Cancel')[0];
userEvent.click(cancelButton); await waitFor(() => userEvent.click(cancelButton));
expect(cancelButton).not.toBeInTheDocument(); expect(cancelButton).not.toBeInTheDocument();
}); });
}); });
@ -123,9 +125,11 @@ describe('Details', () => {
userEvent.click(confirmButton); userEvent.click(confirmButton);
}); });
it('it calls clearTopicMessages on confirm', () => { it('it calls clearTopicMessages on confirm', async () => {
const submitButton = screen.getAllByText('Submit')[0]; const submitButton = screen.getAllByRole('button', {
userEvent.click(submitButton); name: 'Confirm',
})[0];
await waitFor(() => userEvent.click(submitButton));
expect(mockClearTopicMessages).toHaveBeenCalledWith({ expect(mockClearTopicMessages).toHaveBeenCalledWith({
clusterName: mockClusterName, 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]; const cancelButton = screen.getAllByText('Cancel')[0];
userEvent.click(cancelButton); await waitFor(() => userEvent.click(cancelButton));
expect(cancelButton).not.toBeInTheDocument(); 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(); setupComponent();
const deleteTopicButton = screen.getByText(/Remove topic/i); const deleteTopicButton = screen.getByText(/Remove topic/i);
userEvent.click(deleteTopicButton); userEvent.click(deleteTopicButton);
const submitDeleteButton = screen.getByText(/Submit/i); const submitDeleteButton = screen.getByRole('button', { name: 'Confirm' });
userEvent.click(submitDeleteButton); await waitFor(() => userEvent.click(submitDeleteButton));
expect(mockNavigate).toHaveBeenCalledWith('../..'); expect(mockNavigate).toHaveBeenCalledWith('../..');
}); });
@ -184,12 +188,13 @@ describe('Details', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('calling recreation function after click on Submit button', () => { it('calling recreation function after click on Submit button', async () => {
setupComponent(); setupComponent();
const recreateTopicButton = screen.getByText(/Recreate topic/i); const recreateTopicButton = screen.getByText(/Recreate topic/i);
userEvent.click(recreateTopicButton); userEvent.click(recreateTopicButton);
const confirmBtn = screen.getByRole('button', { name: /submit/i }); const confirmBtn = screen.getByRole('button', { name: /Confirm/i });
userEvent.click(confirmBtn);
await waitFor(() => userEvent.click(confirmBtn));
expect(mockRecreateTopic).toBeCalledTimes(1); expect(mockRecreateTopic).toBeCalledTimes(1);
}); });

View file

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

View file

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

View file

@ -20,8 +20,6 @@ const renderComponent = (props?: Partial<Props>) =>
<DangerZone <DangerZone
defaultPartitions={defaultPartitions} defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor} defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased={false}
replicationFactorUpdated={false}
updateTopicPartitionsCount={jest.fn()} updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()} updateTopicReplicationFactor={jest.fn()}
{...props} {...props}
@ -33,7 +31,7 @@ const renderComponent = (props?: Partial<Props>) =>
const clickOnDialogSubmitButton = () => { const clickOnDialogSubmitButton = () => {
userEvent.click( userEvent.click(
within(screen.getByRole('dialog')).getByRole('button', { within(screen.getByRole('dialog')).getByRole('button', {
name: 'Submit', name: 'Confirm',
}) })
); );
}; };
@ -41,7 +39,7 @@ const clickOnDialogSubmitButton = () => {
const checkDialogThenPressCancel = async () => { const checkDialogThenPressCancel = async () => {
const dialog = screen.getByRole('dialog'); const dialog = screen.getByRole('dialog');
expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByRole('dialog')).toBeInTheDocument();
userEvent.click(within(dialog).getByText(/cancel/i)); userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));
await waitFor(() => await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument() 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 () => { it('should close the partitions dialog if he cancel button is pressed', async () => {
renderComponent(); renderComponent();

View file

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

View file

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

View file

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

View file

@ -1,59 +1,64 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
export const ConfirmationModalWrapper = styled.div.attrs({ role: 'dialog' })( export const Wrapper = styled.div.attrs({ role: 'dialog' })`
({ theme }) => css` display: flex;
display: flex; align-items: center;
align-items: center; flex-direction: column;
flex-direction: column; justify-content: center;
justify-content: center; overflow: hidden;
overflow: hidden; position: fixed;
position: fixed; z-index: 40;
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; bottom: 0;
left: 0; left: 0;
position: absolute;
right: 0; right: 0;
top: 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 { 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 { const ConfirmationModal: React.FC = () => {
isOpen?: boolean; const context = React.useContext(ConfirmContext);
title?: React.ReactNode; const isOpen = context?.content && context?.confirm;
onConfirm(): void;
onCancel(): void;
isConfirming?: boolean;
submitBtnText?: string;
}
const ConfirmationModal: React.FC< if (!isOpen) return null;
PropsWithChildren<ConfirmationModalProps>
> = ({
isOpen,
children,
title = 'Confirm the action',
onCancel,
onConfirm,
isConfirming = false,
submitBtnText = 'Submit',
}) => {
const cancelHandler = () => {
if (!isConfirming) onCancel();
};
return isOpen ? ( return (
<ConfirmationModalWrapper> <S.Wrapper role="dialog" aria-label="Confirmation Dialog">
<div onClick={cancelHandler} aria-hidden="true" role="button" /> <S.Overlay onClick={context.cancel} aria-hidden="true" role="button" />
<div> <S.Modal>
<header> <S.Header>Confirm the action</S.Header>
<p>{title}</p> <S.Content>{context.content}</S.Content>
</header> <S.Footer>
<section>{children}</section>
<footer>
<Button <Button
buttonType="secondary" buttonType="secondary"
buttonSize="M" buttonSize="M"
onClick={cancelHandler} onClick={context.cancel}
type="button" type="button"
disabled={isConfirming}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
buttonType="primary" buttonType="primary"
buttonSize="M" buttonSize="M"
onClick={onConfirm} onClick={context.confirm}
type="button" type="button"
disabled={isConfirming}
> >
{submitBtnText} Confirm
</Button> </Button>
</footer> </S.Footer>
</div> </S.Modal>
</ConfirmationModalWrapper> </S.Wrapper>
) : null; );
}; };
export default ConfirmationModal; 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; border: none;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
align-self: center;
`; `;
export const DangerItem = styled.div` export const DangerItem = styled.div`

View file

@ -1,26 +1,36 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu'; import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
import { useConfirm } from 'lib/hooks/useConfirm';
import * as S from './Dropdown.styled'; import * as S from './Dropdown.styled';
interface DropdownItemProps extends PropsWithChildren<MenuItemProps> { interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
danger?: boolean; danger?: boolean;
onClick?(): void; onClick?(): void;
confirm?: React.ReactNode;
} }
const DropdownItem: React.FC<DropdownItemProps> = ({ const DropdownItem: React.FC<DropdownItemProps> = ({
onClick, onClick,
danger, danger,
children, children,
confirm,
...rest ...rest
}) => { }) => {
const confirmation = useConfirm();
const handleClick = (e: ClickEvent) => { const handleClick = (e: ClickEvent) => {
if (!onClick) return; if (!onClick) return;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
e.stopPropagation = true; e.stopPropagation = true;
e.syntheticEvent.stopPropagation(); e.syntheticEvent.stopPropagation();
onClick();
if (confirm) {
confirmation(confirm, onClick);
} else {
onClick();
}
}; };
return ( 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 React, { useEffect } from 'react';
import { render } from 'lib/testHelpers';
import useDataSaver from 'lib/hooks/useDataSaver'; import useDataSaver from 'lib/hooks/useDataSaver';
import { render } from '@testing-library/react';
describe('useDataSaver hook', () => { describe('useDataSaver hook', () => {
const content = { 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, QueryClientProvider,
UseQueryResult, UseQueryResult,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { ConfirmContextProvider } from 'components/contexts/ConfirmContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> { interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
preloadedState?: Partial<RootState>; preloadedState?: Partial<RootState>;
@ -67,13 +69,18 @@ const customRender = (
children, children,
}) => ( }) => (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Provider store={store}> <ConfirmContextProvider>
<TestQueryClientProvider> <Provider store={store}>
<MemoryRouter initialEntries={initialEntries}> <TestQueryClientProvider>
{children} <MemoryRouter initialEntries={initialEntries}>
</MemoryRouter> <div>
</TestQueryClientProvider> {children}
</Provider> <ConfirmationModal />
</div>
</MemoryRouter>
</TestQueryClientProvider>
</Provider>
</ConfirmContextProvider>
</ThemeProvider> </ThemeProvider>
); );
return render(ui, { wrapper: AllTheProviders, ...renderOptions }); return render(ui, { wrapper: AllTheProviders, ...renderOptions });

View file

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

View file

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

View file

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

View file

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