New Confirmation Messages modal (#2376)
* New Confirmation Messages * Fix #2348 * Fix codesmels * fix #2242 * fix #2347
This commit is contained in:
parent
b6e9e43868
commit
2d82b9c0a9
40 changed files with 591 additions and 959 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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('../'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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="."
|
||||||
|
|
|
@ -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>`
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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%;
|
||||||
|
`;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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`
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 = {
|
||||||
|
|
14
kafka-ui-react-app/src/lib/hooks/useConfirm.ts
Normal file
14
kafka-ui-react-app/src/lib/hooks/useConfirm.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
|
@ -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 });
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue