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 DiscordIcon from 'components/common/Icons/DiscordIcon';
|
||||
|
||||
import { ConfirmContextProvider } from './contexts/ConfirmContext';
|
||||
import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
@ -44,84 +47,87 @@ const App: React.FC = () => {
|
|||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalCSS />
|
||||
<S.Layout>
|
||||
<S.Navbar role="navigation" aria-label="Page Header">
|
||||
<S.NavbarBrand>
|
||||
<ConfirmContextProvider>
|
||||
<GlobalCSS />
|
||||
<S.Layout>
|
||||
<S.Navbar role="navigation" aria-label="Page Header">
|
||||
<S.NavbarBrand>
|
||||
<S.NavbarBurger
|
||||
onClick={onBurgerClick}
|
||||
onKeyDown={onBurgerClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="burger"
|
||||
>
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
</S.NavbarBurger>
|
||||
<S.NavbarBrand>
|
||||
<S.NavbarBurger
|
||||
onClick={onBurgerClick}
|
||||
onKeyDown={onBurgerClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="burger"
|
||||
>
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
</S.NavbarBurger>
|
||||
|
||||
<S.Hyperlink to="/">
|
||||
<Logo />
|
||||
UI for Apache Kafka
|
||||
</S.Hyperlink>
|
||||
<S.Hyperlink to="/">
|
||||
<Logo />
|
||||
UI for Apache Kafka
|
||||
</S.Hyperlink>
|
||||
|
||||
<S.NavbarItem>
|
||||
{GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
|
||||
</S.NavbarItem>
|
||||
<S.NavbarItem>
|
||||
{GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
|
||||
</S.NavbarItem>
|
||||
</S.NavbarBrand>
|
||||
</S.NavbarBrand>
|
||||
</S.NavbarBrand>
|
||||
<S.NavbarSocial>
|
||||
<S.LogoutLink href="/logout">
|
||||
<S.LogoutButton buttonType="primary" buttonSize="M">
|
||||
Log out
|
||||
</S.LogoutButton>
|
||||
</S.LogoutLink>
|
||||
<S.SocialLink
|
||||
href="https://github.com/provectus/kafka-ui"
|
||||
target="_blank"
|
||||
>
|
||||
<GitIcon />
|
||||
</S.SocialLink>
|
||||
<S.SocialLink
|
||||
href="https://discord.com/invite/4DWzD7pGE5"
|
||||
target="_blank"
|
||||
>
|
||||
<DiscordIcon />
|
||||
</S.SocialLink>
|
||||
</S.NavbarSocial>
|
||||
</S.Navbar>
|
||||
<S.NavbarSocial>
|
||||
<S.LogoutLink href="/logout">
|
||||
<S.LogoutButton buttonType="primary" buttonSize="M">
|
||||
Log out
|
||||
</S.LogoutButton>
|
||||
</S.LogoutLink>
|
||||
<S.SocialLink
|
||||
href="https://github.com/provectus/kafka-ui"
|
||||
target="_blank"
|
||||
>
|
||||
<GitIcon />
|
||||
</S.SocialLink>
|
||||
<S.SocialLink
|
||||
href="https://discord.com/invite/4DWzD7pGE5"
|
||||
target="_blank"
|
||||
>
|
||||
<DiscordIcon />
|
||||
</S.SocialLink>
|
||||
</S.NavbarSocial>
|
||||
</S.Navbar>
|
||||
|
||||
<S.Container>
|
||||
<S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Nav />
|
||||
</Suspense>
|
||||
</S.Sidebar>
|
||||
<S.Overlay
|
||||
$visible={isSidebarVisible}
|
||||
onClick={closeSidebar}
|
||||
onKeyDown={closeSidebar}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
aria-label="Overlay"
|
||||
/>
|
||||
<Routes>
|
||||
{['/', '/ui', '/ui/clusters'].map((path) => (
|
||||
<Route
|
||||
key="Home" // optional: avoid full re-renders on route changes
|
||||
path={path}
|
||||
element={<Dashboard />}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path={getNonExactPath(clusterPath())}
|
||||
element={<ClusterPage />}
|
||||
<S.Container>
|
||||
<S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Nav />
|
||||
</Suspense>
|
||||
</S.Sidebar>
|
||||
<S.Overlay
|
||||
$visible={isSidebarVisible}
|
||||
onClick={closeSidebar}
|
||||
onKeyDown={closeSidebar}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
aria-label="Overlay"
|
||||
/>
|
||||
</Routes>
|
||||
</S.Container>
|
||||
<Toaster position="bottom-right" />
|
||||
</S.Layout>
|
||||
<Routes>
|
||||
{['/', '/ui', '/ui/clusters'].map((path) => (
|
||||
<Route
|
||||
key="Home" // optional: avoid full re-renders on route changes
|
||||
path={path}
|
||||
element={<Dashboard />}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path={getNonExactPath(clusterPath())}
|
||||
element={<ClusterPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</S.Container>
|
||||
<Toaster position="bottom-right" />
|
||||
</S.Layout>
|
||||
<ConfirmationModal />
|
||||
</ConfirmContextProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { useIsMutating } from '@tanstack/react-query';
|
||||
import { ConnectorState, ConnectorAction } from 'generated-sources';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
import useModal from 'lib/hooks/useModal';
|
||||
import {
|
||||
useConnector,
|
||||
useDeleteConnector,
|
||||
|
@ -15,8 +14,8 @@ import {
|
|||
clusterConnectorsPath,
|
||||
RouterParamsClusterConnectConnector,
|
||||
} from 'lib/paths';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
const ConnectorActionsWrapperStyled = styled.div`
|
||||
display: flex;
|
||||
|
@ -32,22 +31,24 @@ const Actions: React.FC = () => {
|
|||
const isMutating = mutationsNumber > 0;
|
||||
|
||||
const { data: connector } = useConnector(routerProps);
|
||||
|
||||
const {
|
||||
isOpen: isDeleteConnectorConfirmationOpen,
|
||||
setClose: setDeleteConnectorConfirmationClose,
|
||||
setOpen: setDeleteConnectorConfirmationOpen,
|
||||
} = useModal();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const deleteConnectorMutation = useDeleteConnector(routerProps);
|
||||
const deleteConnectorHandler = async () => {
|
||||
try {
|
||||
await deleteConnectorMutation.mutateAsync();
|
||||
navigate(clusterConnectorsPath(routerProps.clusterName));
|
||||
} catch {
|
||||
// do not redirect
|
||||
}
|
||||
};
|
||||
const deleteConnectorHandler = () =>
|
||||
confirm(
|
||||
<>
|
||||
Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
|
||||
connector?
|
||||
</>,
|
||||
async () => {
|
||||
try {
|
||||
await deleteConnectorMutation.mutateAsync();
|
||||
navigate(clusterConnectorsPath(routerProps.clusterName));
|
||||
} catch {
|
||||
// do not redirect
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const stateMutation = useUpdateConnectorState(routerProps);
|
||||
const restartConnectorHandler = () =>
|
||||
|
@ -71,7 +72,7 @@ const Actions: React.FC = () => {
|
|||
onClick={pauseConnectorHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Pause</span>
|
||||
Pause
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
@ -83,7 +84,7 @@ const Actions: React.FC = () => {
|
|||
onClick={resumeConnectorHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Resume</span>
|
||||
Resume
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
@ -94,7 +95,7 @@ const Actions: React.FC = () => {
|
|||
onClick={restartConnectorHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Restart Connector</span>
|
||||
Restart Connector
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
|
@ -103,7 +104,7 @@ const Actions: React.FC = () => {
|
|||
onClick={restartAllTasksHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Restart All Tasks</span>
|
||||
Restart All Tasks
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
|
@ -112,7 +113,7 @@ const Actions: React.FC = () => {
|
|||
onClick={restartFailedTasksHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Restart Failed Tasks</span>
|
||||
Restart Failed Tasks
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
|
@ -125,27 +126,18 @@ const Actions: React.FC = () => {
|
|||
routerProps.connectorName
|
||||
)}
|
||||
>
|
||||
<span>Edit Config</span>
|
||||
Edit Config
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
onClick={setDeleteConnectorConfirmationOpen}
|
||||
onClick={deleteConnectorHandler}
|
||||
disabled={isMutating}
|
||||
>
|
||||
<span>Delete</span>
|
||||
Delete
|
||||
</Button>
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteConnectorConfirmationOpen}
|
||||
onCancel={setDeleteConnectorConfirmationClose}
|
||||
onConfirm={deleteConnectorHandler}
|
||||
isConfirming={isMutating}
|
||||
>
|
||||
Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
|
||||
connector?
|
||||
</ConfirmationModal>
|
||||
</ConnectorActionsWrapperStyled>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { render, WithRoute } from 'lib/testHelpers';
|
|||
import { clusterConnectConnectorPath } from 'lib/paths';
|
||||
import Actions from 'components/Connect/Details/Actions/Actions';
|
||||
import { ConnectorAction, ConnectorState } from 'generated-sources';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
useConnector,
|
||||
|
@ -27,11 +27,6 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({
|
|||
useUpdateConnectorState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'components/common/ConfirmationModal/ConfirmationModal',
|
||||
() => 'mock-ConfirmationModal'
|
||||
);
|
||||
|
||||
const expectActionButtonsExists = () => {
|
||||
expect(screen.getByText('Restart Connector')).toBeInTheDocument();
|
||||
expect(screen.getByText('Restart All Tasks')).toBeInTheDocument();
|
||||
|
@ -116,10 +111,10 @@ describe('Actions', () => {
|
|||
|
||||
it('opens confirmation modal when delete button clicked', async () => {
|
||||
renderComponent();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to remove/i)
|
||||
).toHaveAttribute('isopen', 'true');
|
||||
await waitFor(() =>
|
||||
userEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
);
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls restartConnector when restart button clicked', () => {
|
||||
|
|
|
@ -32,8 +32,8 @@ describe('Config', () => {
|
|||
|
||||
it('is empty when no config', () => {
|
||||
(useConnectorConfig as jest.Mock).mockImplementation(() => ({}));
|
||||
const { container } = renderComponent();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
renderComponent();
|
||||
expect(screen.queryByText('mock-Editor')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders editor', () => {
|
||||
|
|
|
@ -19,8 +19,8 @@ describe('Overview', () => {
|
|||
data: undefined,
|
||||
}));
|
||||
|
||||
const { container } = render(<Overview />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
render(<Overview />);
|
||||
expect(screen.queryByText('Worker')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when connector is loaded', () => {
|
||||
|
|
|
@ -3,13 +3,12 @@ import { FullConnectorInfo } from 'generated-sources';
|
|||
import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { Tag } from 'components/common/Tag/Tag.styled';
|
||||
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
||||
import getTagColor from 'components/common/Tag/getTagColor';
|
||||
import useModal from 'lib/hooks/useModal';
|
||||
import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
|
||||
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import * as S from './List.styled';
|
||||
|
||||
|
@ -31,16 +30,22 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
failedTasksCount,
|
||||
},
|
||||
}) => {
|
||||
const { isOpen, setClose, setOpen } = useModal();
|
||||
const confirm = useConfirm();
|
||||
const deleteMutation = useDeleteConnector({
|
||||
clusterName,
|
||||
connectName: connect,
|
||||
connectorName: name,
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteMutation.mutateAsync();
|
||||
setClose();
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
<>
|
||||
Are you sure want to remove <b>{name}</b> connector?
|
||||
</>,
|
||||
async () => {
|
||||
await deleteMutation.mutateAsync();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const runningTasks = React.useMemo(() => {
|
||||
|
@ -78,18 +83,11 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
<td>
|
||||
<div>
|
||||
<Dropdown>
|
||||
<DropdownItem onClick={setOpen} danger>
|
||||
<DropdownItem onClick={handleDelete} danger>
|
||||
Remove Connector
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={isOpen}
|
||||
onCancel={setClose}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
Are you sure want to remove <b>{name}</b> connector?
|
||||
</ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
import React from 'react';
|
||||
import { connectors } from 'lib/fixtures/kafkaConnect';
|
||||
import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
jest.mock(
|
||||
'components/common/ConfirmationModal/ConfirmationModal',
|
||||
() => 'mock-ConfirmationModal'
|
||||
);
|
||||
|
||||
describe('Connectors ListItem', () => {
|
||||
const connector = connectors[0];
|
||||
const setupWrapper = (props: Partial<ListItemProps> = {}) => (
|
||||
|
@ -21,25 +14,6 @@ describe('Connectors ListItem', () => {
|
|||
</table>
|
||||
);
|
||||
|
||||
const onCancel = jest.fn();
|
||||
const onConfirm = jest.fn();
|
||||
const confirmationModal = (props: Partial<ListItemProps> = {}) => (
|
||||
<ConfirmationModal onCancel={onCancel} onConfirm={onConfirm}>
|
||||
<button type="button" id="cancel" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
{props.clusterName ? (
|
||||
<button type="button" id="delete" onClick={onConfirm}>
|
||||
Confirm
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" id="delete">
|
||||
Confirm
|
||||
</button>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
|
||||
it('renders item', () => {
|
||||
render(setupWrapper());
|
||||
expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2');
|
||||
|
@ -76,22 +50,4 @@ describe('Connectors ListItem', () => {
|
|||
);
|
||||
expect(screen.getAllByRole('cell')[6]).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('handles cancel', async () => {
|
||||
render(confirmationModal());
|
||||
userEvent.click(screen.getByText('Cancel'));
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete', () => {
|
||||
render(confirmationModal({ clusterName: 'test' }));
|
||||
userEvent.click(screen.getByText('Confirm'));
|
||||
expect(onConfirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete when clusterName is not present', () => {
|
||||
render(confirmationModal({ clusterName: undefined }));
|
||||
userEvent.click(screen.getByText('Confirm'));
|
||||
expect(onConfirm).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
ClusterGroupParam,
|
||||
} from 'lib/paths';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
|
@ -38,17 +37,14 @@ const Details: React.FC = () => {
|
|||
const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
|
||||
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
|
||||
|
||||
const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
|
||||
}, [clusterName, consumerGroupID, dispatch]);
|
||||
|
||||
const onDelete = () => {
|
||||
setIsConfirmationModalVisible(false);
|
||||
dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDeleted) {
|
||||
navigate('../');
|
||||
|
@ -73,7 +69,8 @@ const Details: React.FC = () => {
|
|||
<Dropdown>
|
||||
<DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={() => setIsConfirmationModalVisible(true)}
|
||||
confirm="Are you sure you want to delete this consumer group?"
|
||||
onClick={onDelete}
|
||||
danger
|
||||
>
|
||||
Delete consumer group
|
||||
|
@ -119,13 +116,6 @@ const Details: React.FC = () => {
|
|||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmationModalVisible}
|
||||
onCancel={() => setIsConfirmationModalVisible(false)}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -97,12 +97,13 @@ describe('Details component', () => {
|
|||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
200
|
||||
);
|
||||
await act(() => {
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
|
||||
});
|
||||
expect(deleteConsumerGroupMock.called()).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(mockNavigate).toHaveBeenLastCalledWith('../');
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenLastCalledWith('../'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
clusterSchemaSchemaComparePageRelativePath,
|
||||
} from 'lib/paths';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
|
@ -38,10 +37,6 @@ const Details: React.FC = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
|
||||
const [
|
||||
isDeleteSchemaConfirmationVisible,
|
||||
setDeleteSchemaConfirmationVisible,
|
||||
] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchLatestSchema({ clusterName, subject }));
|
||||
|
@ -62,7 +57,7 @@ const Details: React.FC = () => {
|
|||
const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
|
||||
const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
|
||||
|
||||
const onDelete = async () => {
|
||||
const deleteHandler = async () => {
|
||||
try {
|
||||
await schemasApiClient.deleteSchema({
|
||||
clusterName,
|
||||
|
@ -101,19 +96,17 @@ const Details: React.FC = () => {
|
|||
</Button>
|
||||
<Dropdown>
|
||||
<DropdownItem
|
||||
onClick={() => setDeleteSchemaConfirmationVisible(true)}
|
||||
confirm={
|
||||
<>
|
||||
Are you sure want to remove <b>{subject}</b> schema?
|
||||
</>
|
||||
}
|
||||
onClick={deleteHandler}
|
||||
danger
|
||||
>
|
||||
Remove schema
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteSchemaConfirmationVisible}
|
||||
onCancel={() => setDeleteSchemaConfirmationVisible(false)}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
Are you sure want to remove <b>{subject}</b> schema?
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)}
|
||||
</PageHeading>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import Select from 'components/common/Select/Select';
|
||||
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
||||
import { useAppDispatch } from 'lib/hooks/redux';
|
||||
|
@ -10,6 +9,7 @@ import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
|
|||
import { ClusterNameRoute } from 'lib/paths';
|
||||
import { schemasApiClient } from 'lib/api';
|
||||
import { showServerError } from 'lib/errorHandling';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import * as S from './GlobalSchemaSelector.styled';
|
||||
|
||||
|
@ -18,17 +18,12 @@ const GlobalSchemaSelector: React.FC = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const [searchText] = useSearch();
|
||||
const { page, perPage } = usePagination();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
||||
React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
|
||||
const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
|
||||
CompatibilityLevelCompatibilityEnum | undefined
|
||||
>();
|
||||
|
||||
const [isFetching, setIsFetching] = React.useState(false);
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
const [isConfirmationVisible, setIsConfirmationVisible] =
|
||||
React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
@ -49,29 +44,30 @@ const GlobalSchemaSelector: React.FC = () => {
|
|||
}, [clusterName]);
|
||||
|
||||
const handleChangeCompatibilityLevel = (level: string | number) => {
|
||||
setNextCompatibilityLevel(level as CompatibilityLevelCompatibilityEnum);
|
||||
setIsConfirmationVisible(true);
|
||||
};
|
||||
|
||||
const handleUpdateCompatibilityLevel = async () => {
|
||||
setIsUpdating(true);
|
||||
if (nextCompatibilityLevel) {
|
||||
try {
|
||||
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
|
||||
clusterName,
|
||||
compatibilityLevel: { compatibility: nextCompatibilityLevel },
|
||||
});
|
||||
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
||||
setNextCompatibilityLevel(undefined);
|
||||
setIsConfirmationVisible(false);
|
||||
dispatch(
|
||||
fetchSchemas({ clusterName, page, perPage, search: searchText })
|
||||
);
|
||||
} catch (e) {
|
||||
showServerError(e as Response);
|
||||
const nextLevel = level as CompatibilityLevelCompatibilityEnum;
|
||||
confirm(
|
||||
<>
|
||||
Are you sure you want to update the global compatibility level and set
|
||||
it to <b>{nextLevel}</b>? This may affect the compatibility levels of
|
||||
the schemas.
|
||||
</>,
|
||||
async () => {
|
||||
try {
|
||||
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
|
||||
clusterName,
|
||||
compatibilityLevel: {
|
||||
compatibility: nextLevel,
|
||||
},
|
||||
});
|
||||
setCurrentCompatibilityLevel(nextLevel);
|
||||
dispatch(
|
||||
fetchSchemas({ clusterName, page, perPage, search: searchText })
|
||||
);
|
||||
} catch (e) {
|
||||
showServerError(e as Response);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsUpdating(false);
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentCompatibilityLevel) return null;
|
||||
|
@ -84,21 +80,11 @@ const GlobalSchemaSelector: React.FC = () => {
|
|||
defaultValue={currentCompatibilityLevel}
|
||||
minWidth="200px"
|
||||
onChange={handleChangeCompatibilityLevel}
|
||||
disabled={isFetching || isUpdating || isConfirmationVisible}
|
||||
disabled={isFetching}
|
||||
options={Object.keys(CompatibilityLevelCompatibilityEnum).map(
|
||||
(level) => ({ value: level, label: level })
|
||||
)}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmationVisible}
|
||||
onCancel={() => setIsConfirmationVisible(false)}
|
||||
onConfirm={handleUpdateCompatibilityLevel}
|
||||
isConfirming={isUpdating}
|
||||
>
|
||||
Are you sure you want to update the global compatibility level and set
|
||||
it to <b>{nextCompatibilityLevel}</b>? This may affect the compatibility
|
||||
levels of the schemas.
|
||||
</ConfirmationModal>
|
||||
</S.Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -86,7 +86,7 @@ describe('GlobalSchemaSelector', () => {
|
|||
200
|
||||
);
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
|
||||
});
|
||||
await waitFor(() => expect(putNewCompatibilityMock.called()).toBeTruthy());
|
||||
await waitFor(() => expect(getSchemasMock.called()).toBeTruthy());
|
||||
|
@ -94,6 +94,9 @@ describe('GlobalSchemaSelector', () => {
|
|||
await waitFor(() =>
|
||||
expect(screen.queryByText('Confirm the action')).not.toBeInTheDocument()
|
||||
);
|
||||
expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD);
|
||||
|
||||
await waitFor(() =>
|
||||
expectOptionIsSelected(CompatibilityLevelCompatibilityEnum.FORWARD)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,11 @@ import {
|
|||
TopicColumnsToSort,
|
||||
} from 'generated-sources';
|
||||
import { useAppDispatch } from 'lib/hooks/redux';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { TableCellProps } from 'components/common/SmartTable/TableColumn';
|
||||
import { TopicWithDetailedInfo } from 'redux/interfaces';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import * as S from 'components/Topics/List/List.styled';
|
||||
import { ClusterNameRoute } from 'lib/paths';
|
||||
import useModal from 'lib/hooks/useModal';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
import {
|
||||
deleteTopic,
|
||||
|
@ -46,85 +44,61 @@ const ActionsCell: React.FC<
|
|||
const dispatch = useAppDispatch();
|
||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||
|
||||
const {
|
||||
isOpen: isDeleteTopicModalOpen,
|
||||
setClose: closeDeleteTopicModal,
|
||||
setOpen: openDeleteTopicModal,
|
||||
} = useModal(false);
|
||||
|
||||
const {
|
||||
isOpen: isRecreateTopicModalOpen,
|
||||
setClose: closeRecreateTopicModal,
|
||||
setOpen: openRecreateTopicModal,
|
||||
} = useModal(false);
|
||||
|
||||
const {
|
||||
isOpen: isClearMessagesModalOpen,
|
||||
setClose: closeClearMessagesModal,
|
||||
setOpen: openClearMessagesModal,
|
||||
} = useModal(false);
|
||||
|
||||
const isHidden = internal || isReadOnly || !hovered;
|
||||
|
||||
const deleteTopicHandler = () => {
|
||||
dispatch(deleteTopic({ clusterName, topicName: name }));
|
||||
closeDeleteTopicModal();
|
||||
};
|
||||
|
||||
const clearTopicMessagesHandler = () => {
|
||||
dispatch(clearTopicMessages({ clusterName, topicName: name }));
|
||||
dispatch(fetchTopicsList(topicsListParams));
|
||||
closeClearMessagesModal();
|
||||
};
|
||||
|
||||
const recreateTopicHandler = () => {
|
||||
dispatch(recreateTopic({ clusterName, topicName: name }));
|
||||
closeRecreateTopicModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<S.ActionsContainer>
|
||||
{!isHidden && (
|
||||
<Dropdown>
|
||||
{cleanUpPolicy === CleanUpPolicy.DELETE && (
|
||||
<DropdownItem onClick={openClearMessagesModal} danger>
|
||||
Clear Messages
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={openRecreateTopicModal} danger>
|
||||
Recreate Topic
|
||||
<S.ActionsContainer>
|
||||
{!isHidden && (
|
||||
<Dropdown>
|
||||
{cleanUpPolicy === CleanUpPolicy.DELETE && (
|
||||
<DropdownItem
|
||||
onClick={clearTopicMessagesHandler}
|
||||
confirm="Are you sure want to clear topic messages?"
|
||||
danger
|
||||
>
|
||||
Clear Messages
|
||||
</DropdownItem>
|
||||
{isTopicDeletionAllowed && (
|
||||
<DropdownItem onClick={openDeleteTopicModal} danger>
|
||||
Remove Topic
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
</S.ActionsContainer>
|
||||
<ConfirmationModal
|
||||
isOpen={isClearMessagesModalOpen}
|
||||
onCancel={closeClearMessagesModal}
|
||||
onConfirm={clearTopicMessagesHandler}
|
||||
>
|
||||
Are you sure want to clear topic messages?
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteTopicModalOpen}
|
||||
onCancel={closeDeleteTopicModal}
|
||||
onConfirm={deleteTopicHandler}
|
||||
>
|
||||
Are you sure want to remove <b>{name}</b> topic?
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
isOpen={isRecreateTopicModalOpen}
|
||||
onCancel={closeRecreateTopicModal}
|
||||
onConfirm={recreateTopicHandler}
|
||||
>
|
||||
Are you sure to recreate <b>{name}</b> topic?
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)}
|
||||
<DropdownItem
|
||||
onClick={recreateTopicHandler}
|
||||
confirm={
|
||||
<>
|
||||
Are you sure to recreate <b>{name}</b> topic?
|
||||
</>
|
||||
}
|
||||
danger
|
||||
>
|
||||
Recreate Topic
|
||||
</DropdownItem>
|
||||
{isTopicDeletionAllowed && (
|
||||
<DropdownItem
|
||||
onClick={deleteTopicHandler}
|
||||
confirm={
|
||||
<>
|
||||
Are you sure want to remove <b>{name}</b> topic?
|
||||
</>
|
||||
}
|
||||
danger
|
||||
>
|
||||
Remove Topic
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
</S.ActionsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
import usePagination from 'lib/hooks/usePagination';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import {
|
||||
GetTopicsRequest,
|
||||
SortOrder,
|
||||
|
@ -30,6 +29,7 @@ import { SmartTable } from 'components/common/SmartTable/SmartTable';
|
|||
import { TableColumn } from 'components/common/SmartTable/TableColumn';
|
||||
import { useTableState } from 'lib/hooks/useTableState';
|
||||
import PlusIcon from 'components/common/Icons/PlusIcon';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import {
|
||||
MessagesCell,
|
||||
|
@ -88,6 +88,7 @@ const List: React.FC<TopicsListProps> = ({
|
|||
page || null
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const topicsListParams = React.useMemo(
|
||||
() => ({
|
||||
|
@ -148,16 +149,6 @@ const List: React.FC<TopicsListProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const [confirmationModal, setConfirmationModal] = React.useState<
|
||||
'' | 'deleteTopics' | 'purgeMessages'
|
||||
>('');
|
||||
|
||||
const [confirmationModalText, setConfirmationModalText] =
|
||||
React.useState<string>('');
|
||||
const closeConfirmationModal = () => {
|
||||
setConfirmationModal('');
|
||||
};
|
||||
|
||||
const clearSelectedTopics = () => tableState.toggleSelection(false);
|
||||
|
||||
const searchHandler = (searchString: string) => {
|
||||
|
@ -171,16 +162,26 @@ const List: React.FC<TopicsListProps> = ({
|
|||
search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
|
||||
});
|
||||
};
|
||||
const deleteOrPurgeConfirmationHandler = () => {
|
||||
|
||||
const deleteTopicsHandler = () => {
|
||||
const selectedIds = Array.from(tableState.selectedIds);
|
||||
if (confirmationModal === 'deleteTopics') {
|
||||
confirm('Are you sure you want to remove selected topics?', () => {
|
||||
deleteTopics({ clusterName, topicNames: selectedIds });
|
||||
} else {
|
||||
clearTopicsMessages({ clusterName, topicNames: selectedIds });
|
||||
}
|
||||
closeConfirmationModal();
|
||||
clearSelectedTopics();
|
||||
fetchTopicsList(topicsListParams);
|
||||
clearSelectedTopics();
|
||||
fetchTopicsList(topicsListParams);
|
||||
});
|
||||
};
|
||||
|
||||
const purgeTopicsHandler = () => {
|
||||
const selectedIds = Array.from(tableState.selectedIds);
|
||||
confirm(
|
||||
'Are you sure you want to purge messages of selected topics?',
|
||||
() => {
|
||||
clearTopicsMessages({ clusterName, topicNames: selectedIds });
|
||||
clearSelectedTopics();
|
||||
fetchTopicsList(topicsListParams);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -220,54 +221,35 @@ const List: React.FC<TopicsListProps> = ({
|
|||
) : (
|
||||
<div>
|
||||
{tableState.selectedCount > 0 && (
|
||||
<>
|
||||
<ControlPanelWrapper data-testid="delete-buttons">
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
onClick={() => {
|
||||
setConfirmationModal('deleteTopics');
|
||||
setConfirmationModalText(
|
||||
'Are you sure you want to remove selected topics?'
|
||||
);
|
||||
}}
|
||||
>
|
||||
Delete selected topics
|
||||
</Button>
|
||||
{tableState.selectedCount === 1 && (
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
to={{
|
||||
pathname: clusterTopicCopyRelativePath,
|
||||
search: `?${getSelectedTopic()}`,
|
||||
}}
|
||||
>
|
||||
Copy selected topic
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
onClick={() => {
|
||||
setConfirmationModal('purgeMessages');
|
||||
setConfirmationModalText(
|
||||
'Are you sure you want to purge messages of selected topics?'
|
||||
);
|
||||
}}
|
||||
>
|
||||
Purge messages of selected topics
|
||||
</Button>
|
||||
</ControlPanelWrapper>
|
||||
<ConfirmationModal
|
||||
isOpen={confirmationModal !== ''}
|
||||
onCancel={closeConfirmationModal}
|
||||
onConfirm={deleteOrPurgeConfirmationHandler}
|
||||
<ControlPanelWrapper data-testid="delete-buttons">
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
onClick={deleteTopicsHandler}
|
||||
>
|
||||
{confirmationModalText}
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
Delete selected topics
|
||||
</Button>
|
||||
{tableState.selectedCount === 1 && (
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
to={{
|
||||
pathname: clusterTopicCopyRelativePath,
|
||||
search: `?${getSelectedTopic()}`,
|
||||
}}
|
||||
>
|
||||
Copy selected topic
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
onClick={purgeTopicsHandler}
|
||||
>
|
||||
Purge messages of selected topics
|
||||
</Button>
|
||||
</ControlPanelWrapper>
|
||||
)}
|
||||
<SmartTable
|
||||
selectable={!isReadOnly}
|
||||
|
|
|
@ -280,9 +280,9 @@ describe('List', () => {
|
|||
)[buttonIndex];
|
||||
userEvent.click(buttonClickedElement);
|
||||
|
||||
const modal = screen.getByRole('dialog');
|
||||
const modal = await screen.findByRole('dialog');
|
||||
expect(within(modal).getByText(confirmationText)).toBeInTheDocument();
|
||||
userEvent.click(within(modal).getByRole('button', { name: 'Submit' }));
|
||||
userEvent.click(within(modal).getByRole('button', { name: 'Confirm' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('delete-buttons')).not.toBeInTheDocument();
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
clusterTopicSendMessageRelativePath,
|
||||
} from 'lib/paths';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import styled from 'styled-components';
|
||||
|
@ -60,42 +59,22 @@ const Details: React.FC<Props> = ({
|
|||
clearTopicMessages,
|
||||
}) => {
|
||||
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
|
||||
|
||||
const isInternal = useAppSelector((state) =>
|
||||
getIsTopicInternal(state, topicName)
|
||||
);
|
||||
const isDeletePolicy = useAppSelector((state) =>
|
||||
getIsTopicDeletePolicy(state, topicName)
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isReadOnly, isTopicDeletionAllowed } =
|
||||
React.useContext(ClusterContext);
|
||||
|
||||
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
|
||||
React.useState(false);
|
||||
const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
|
||||
React.useState(false);
|
||||
const [
|
||||
isRecreateTopicConfirmationVisible,
|
||||
setRecreateTopicConfirmationVisible,
|
||||
] = React.useState(false);
|
||||
const deleteTopicHandler = () => {
|
||||
deleteTopic({ clusterName, topicName });
|
||||
setDeleteTopicConfirmationVisible(false);
|
||||
navigate('../..');
|
||||
};
|
||||
|
||||
const clearTopicMessagesHandler = () => {
|
||||
clearTopicMessages({ clusterName, topicName });
|
||||
setClearTopicConfirmationVisible(false);
|
||||
};
|
||||
|
||||
const recreateTopicHandler = () => {
|
||||
recreateTopic({ clusterName, topicName });
|
||||
setRecreateTopicConfirmationVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeading text={topicName}>
|
||||
|
@ -131,26 +110,44 @@ const Details: React.FC<Props> = ({
|
|||
especially important consequences.
|
||||
</DropdownItemHint>
|
||||
</DropdownItem>
|
||||
{isDeletePolicy && (
|
||||
<DropdownItem
|
||||
disabled={!isDeletePolicy}
|
||||
onClick={() =>
|
||||
clearTopicMessages({ clusterName, topicName })
|
||||
}
|
||||
confirm="Are you sure want to clear topic messages?"
|
||||
danger
|
||||
>
|
||||
Clear messages
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem
|
||||
disabled={!isDeletePolicy}
|
||||
onClick={() => setClearTopicConfirmationVisible(true)}
|
||||
danger
|
||||
>
|
||||
Clear messages
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={() => setRecreateTopicConfirmationVisible(true)}
|
||||
onClick={() => recreateTopic({ clusterName, topicName })}
|
||||
confirm={
|
||||
<>
|
||||
Are you sure want to recreate <b>{topicName}</b>{' '}
|
||||
topic?
|
||||
</>
|
||||
}
|
||||
danger
|
||||
>
|
||||
Recreate Topic
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
disabled={!isTopicDeletionAllowed}
|
||||
onClick={() => setDeleteTopicConfirmationVisible(true)}
|
||||
danger
|
||||
>
|
||||
Remove Topic
|
||||
</DropdownItem>
|
||||
{isTopicDeletionAllowed && (
|
||||
<DropdownItem
|
||||
onClick={deleteTopicHandler}
|
||||
confirm={
|
||||
<>
|
||||
Are you sure want to remove <b>{topicName}</b>{' '}
|
||||
topic?
|
||||
</>
|
||||
}
|
||||
danger
|
||||
>
|
||||
Remove Topic
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
|
@ -158,27 +155,6 @@ const Details: React.FC<Props> = ({
|
|||
)}
|
||||
</HeaderControlsWrapper>
|
||||
</PageHeading>
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteTopicConfirmationVisible}
|
||||
onCancel={() => setDeleteTopicConfirmationVisible(false)}
|
||||
onConfirm={deleteTopicHandler}
|
||||
>
|
||||
Are you sure want to remove <b>{topicName}</b> topic?
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
isOpen={isClearTopicConfirmationVisible}
|
||||
onCancel={() => setClearTopicConfirmationVisible(false)}
|
||||
onConfirm={clearTopicMessagesHandler}
|
||||
>
|
||||
Are you sure want to clear topic messages?
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
isOpen={isRecreateTopicConfirmationVisible}
|
||||
onCancel={() => setRecreateTopicConfirmationVisible(false)}
|
||||
onConfirm={recreateTopicHandler}
|
||||
>
|
||||
Are you sure want to recreate <b>{topicName}</b> topic?
|
||||
</ConfirmationModal>
|
||||
<Navbar role="navigation">
|
||||
<NavLink
|
||||
to="."
|
||||
|
|
|
@ -298,13 +298,6 @@ export const DeleteSavedFilterIcon = styled.div`
|
|||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
export const ConfirmDeletionText = styled.h3`
|
||||
color: ${({ theme }) => theme.modal.deletionTextColor};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 16px 0;
|
||||
`;
|
||||
|
||||
export const MessageLoading = styled.div.attrs({
|
||||
role: 'contentLoader',
|
||||
})<MessageLoadingProps>`
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import useModal from 'lib/hooks/useModal';
|
||||
import DeleteIcon from 'components/common/Icons/DeleteIcon';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import * as S from './Filters.styled';
|
||||
import { MessageFilters } from './Filters';
|
||||
|
@ -24,9 +23,8 @@ const SavedFilters: FC<Props> = ({
|
|||
closeModal,
|
||||
onGoBack,
|
||||
}) => {
|
||||
const { isOpen, setOpen, setClose } = useModal();
|
||||
const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
|
||||
const [selectedFilter, setSelectedFilter] = React.useState(-1);
|
||||
const confirm = useConfirm();
|
||||
|
||||
const activeFilter = () => {
|
||||
if (selectedFilter > -1) {
|
||||
|
@ -36,26 +34,13 @@ const SavedFilters: FC<Props> = ({
|
|||
};
|
||||
|
||||
const deleteFilterHandler = (index: number) => {
|
||||
setOpen();
|
||||
setDeleteIndex(index);
|
||||
confirm(<>Are you sure want to remove {filters[index]?.name}?</>, () => {
|
||||
deleteFilter(index);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmationModal
|
||||
isOpen={isOpen}
|
||||
title="Confirm deletion"
|
||||
onConfirm={() => {
|
||||
deleteFilter(deleteIndex);
|
||||
setClose();
|
||||
}}
|
||||
onCancel={setClose}
|
||||
submitBtnText="Delete"
|
||||
>
|
||||
<S.ConfirmDeletionText>
|
||||
Are you sure want to remove {filters[deleteIndex]?.name}?
|
||||
</S.ConfirmDeletionText>
|
||||
</ConfirmationModal>
|
||||
<S.BackToCustomText onClick={onGoBack}>
|
||||
Back To custom filters
|
||||
</S.BackToCustomText>
|
||||
|
@ -86,7 +71,6 @@ const SavedFilters: FC<Props> = ({
|
|||
buttonType="secondary"
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
disabled={isOpen}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
@ -95,7 +79,6 @@ const SavedFilters: FC<Props> = ({
|
|||
buttonType="primary"
|
||||
type="button"
|
||||
onClick={activeFilter}
|
||||
disabled={isOpen}
|
||||
>
|
||||
Select filter
|
||||
</Button>
|
||||
|
|
|
@ -3,7 +3,12 @@ import SavedFilters, {
|
|||
Props,
|
||||
} from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
|
||||
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import {
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
|
@ -11,8 +16,8 @@ jest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');
|
|||
|
||||
describe('SavedFilter Component', () => {
|
||||
const mockFilters: MessageFilters[] = [
|
||||
{ name: 'name', code: 'code' },
|
||||
{ name: 'name1', code: 'code1' },
|
||||
{ name: 'My Filter', code: 'code' },
|
||||
{ name: 'One More Filter', code: 'code1' },
|
||||
];
|
||||
|
||||
const setUpComponent = (props: Partial<Props> = {}) => {
|
||||
|
@ -125,11 +130,11 @@ describe('SavedFilter Component', () => {
|
|||
const modelDialog = screen.getByRole('dialog');
|
||||
expect(modelDialog).toBeInTheDocument();
|
||||
expect(
|
||||
within(modelDialog).getByText(/Confirm deletion/i)
|
||||
within(modelDialog).getByText('Are you sure want to remove My Filter?')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Close Confirmations deletion modal with button', () => {
|
||||
it('Close Confirmations deletion modal with button', async () => {
|
||||
setUpComponent({ deleteFilter: deleteMock });
|
||||
const savedFilters = getSavedFilters();
|
||||
const deleteIcons = screen.getAllByText('mock-DeleteIcon');
|
||||
|
@ -142,11 +147,11 @@ describe('SavedFilter Component', () => {
|
|||
const cancelButton = within(modelDialog).getByRole('button', {
|
||||
name: /Cancel/i,
|
||||
});
|
||||
userEvent.click(cancelButton);
|
||||
await waitFor(() => userEvent.click(cancelButton));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Delete the saved filter', () => {
|
||||
it('Delete the saved filter', async () => {
|
||||
setUpComponent({ deleteFilter: deleteMock });
|
||||
const savedFilters = getSavedFilters();
|
||||
const deleteIcons = screen.getAllByText('mock-DeleteIcon');
|
||||
|
@ -154,9 +159,11 @@ describe('SavedFilter Component', () => {
|
|||
userEvent.hover(savedFilters[0]);
|
||||
userEvent.click(deleteIcons[0]);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: /Delete/i }));
|
||||
await waitFor(() =>
|
||||
userEvent.click(screen.getByRole('button', { name: 'Confirm' }))
|
||||
);
|
||||
expect(deleteMock).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import Details from 'components/Topics/Topic/Details/Details';
|
||||
|
@ -97,9 +97,12 @@ describe('Details', () => {
|
|||
userEvent.click(openModalButton);
|
||||
});
|
||||
|
||||
it('calls deleteTopic on confirm', () => {
|
||||
const submitButton = screen.getAllByText('Submit')[0];
|
||||
userEvent.click(submitButton);
|
||||
it('calls deleteTopic on confirm', async () => {
|
||||
const submitButton = screen.getAllByRole('button', {
|
||||
name: 'Confirm',
|
||||
})[0];
|
||||
|
||||
await waitFor(() => userEvent.click(submitButton));
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledWith({
|
||||
clusterName: mockClusterName,
|
||||
|
@ -107,10 +110,9 @@ describe('Details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('closes the modal when cancel button is clicked', () => {
|
||||
it('closes the modal when cancel button is clicked', async () => {
|
||||
const cancelButton = screen.getAllByText('Cancel')[0];
|
||||
userEvent.click(cancelButton);
|
||||
|
||||
await waitFor(() => userEvent.click(cancelButton));
|
||||
expect(cancelButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -123,9 +125,11 @@ describe('Details', () => {
|
|||
userEvent.click(confirmButton);
|
||||
});
|
||||
|
||||
it('it calls clearTopicMessages on confirm', () => {
|
||||
const submitButton = screen.getAllByText('Submit')[0];
|
||||
userEvent.click(submitButton);
|
||||
it('it calls clearTopicMessages on confirm', async () => {
|
||||
const submitButton = screen.getAllByRole('button', {
|
||||
name: 'Confirm',
|
||||
})[0];
|
||||
await waitFor(() => userEvent.click(submitButton));
|
||||
|
||||
expect(mockClearTopicMessages).toHaveBeenCalledWith({
|
||||
clusterName: mockClusterName,
|
||||
|
@ -133,9 +137,9 @@ describe('Details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('closes the modal when cancel button is clicked', () => {
|
||||
it('closes the modal when cancel button is clicked', async () => {
|
||||
const cancelButton = screen.getAllByText('Cancel')[0];
|
||||
userEvent.click(cancelButton);
|
||||
await waitFor(() => userEvent.click(cancelButton));
|
||||
|
||||
expect(cancelButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -152,14 +156,14 @@ describe('Details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('redirects to the correct route if topic is deleted', () => {
|
||||
it('redirects to the correct route if topic is deleted', async () => {
|
||||
setupComponent();
|
||||
|
||||
const deleteTopicButton = screen.getByText(/Remove topic/i);
|
||||
userEvent.click(deleteTopicButton);
|
||||
|
||||
const submitDeleteButton = screen.getByText(/Submit/i);
|
||||
userEvent.click(submitDeleteButton);
|
||||
const submitDeleteButton = screen.getByRole('button', { name: 'Confirm' });
|
||||
await waitFor(() => userEvent.click(submitDeleteButton));
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('../..');
|
||||
});
|
||||
|
@ -184,12 +188,13 @@ describe('Details', () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calling recreation function after click on Submit button', () => {
|
||||
it('calling recreation function after click on Submit button', async () => {
|
||||
setupComponent();
|
||||
const recreateTopicButton = screen.getByText(/Recreate topic/i);
|
||||
userEvent.click(recreateTopicButton);
|
||||
const confirmBtn = screen.getByRole('button', { name: /submit/i });
|
||||
userEvent.click(confirmBtn);
|
||||
const confirmBtn = screen.getByRole('button', { name: /Confirm/i });
|
||||
|
||||
await waitFor(() => userEvent.click(confirmBtn));
|
||||
expect(mockRecreateTopic).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import Input from 'components/common/Input/Input';
|
||||
import { FormError } from 'components/common/Input/Input.styled';
|
||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||
|
@ -9,14 +8,13 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||
import { RouteParamsClusterTopic } from 'lib/paths';
|
||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import * as S from './DangerZone.styled';
|
||||
|
||||
export interface Props {
|
||||
defaultPartitions: number;
|
||||
defaultReplicationFactor: number;
|
||||
partitionsCountIncreased: boolean;
|
||||
replicationFactorUpdated: boolean;
|
||||
updateTopicPartitionsCount: (payload: {
|
||||
clusterName: ClusterName;
|
||||
topicName: TopicName;
|
||||
|
@ -32,19 +30,10 @@ export interface Props {
|
|||
const DangerZone: React.FC<Props> = ({
|
||||
defaultPartitions,
|
||||
defaultReplicationFactor,
|
||||
partitionsCountIncreased,
|
||||
replicationFactorUpdated,
|
||||
updateTopicPartitionsCount,
|
||||
updateTopicReplicationFactor,
|
||||
}) => {
|
||||
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
|
||||
|
||||
const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
|
||||
React.useState<boolean>(false);
|
||||
const [
|
||||
isReplicationFactorConfirmationVisible,
|
||||
setIsReplicationFactorConfirmationVisible,
|
||||
] = React.useState<boolean>(false);
|
||||
const [partitions, setPartitions] = React.useState<number>(defaultPartitions);
|
||||
const [replicationFactor, setReplicationFactor] = React.useState<number>(
|
||||
defaultReplicationFactor
|
||||
|
@ -62,6 +51,29 @@ const DangerZone: React.FC<Props> = ({
|
|||
},
|
||||
});
|
||||
|
||||
const confirm = useConfirm();
|
||||
|
||||
const confirmPartitionsChange = () =>
|
||||
confirm(
|
||||
`Are you sure you want to increase the number of partitions?
|
||||
Do it only if you 100% know what you are doing!`,
|
||||
() =>
|
||||
updateTopicPartitionsCount({
|
||||
clusterName,
|
||||
topicName,
|
||||
partitions: partitionsMethods.getValues('partitions'),
|
||||
})
|
||||
);
|
||||
const confirmReplicationFactorChange = () =>
|
||||
confirm('Are you sure you want to update the replication factor?', () =>
|
||||
updateTopicReplicationFactor({
|
||||
clusterName,
|
||||
topicName,
|
||||
replicationFactor:
|
||||
replicationFactorMethods.getValues('replicationFactor'),
|
||||
})
|
||||
);
|
||||
|
||||
const validatePartitions = (data: { partitions: number }) => {
|
||||
if (data.partitions < defaultPartitions) {
|
||||
partitionsMethods.setError('partitions', {
|
||||
|
@ -70,42 +82,15 @@ const DangerZone: React.FC<Props> = ({
|
|||
});
|
||||
} else {
|
||||
setPartitions(data.partitions);
|
||||
setIsPartitionsConfirmationVisible(true);
|
||||
confirmPartitionsChange();
|
||||
}
|
||||
};
|
||||
|
||||
const validateReplicationFactor = (data: { replicationFactor: number }) => {
|
||||
setReplicationFactor(data.replicationFactor);
|
||||
setIsReplicationFactorConfirmationVisible(true);
|
||||
confirmReplicationFactorChange();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (partitionsCountIncreased) {
|
||||
setIsPartitionsConfirmationVisible(false);
|
||||
}
|
||||
}, [partitionsCountIncreased]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (replicationFactorUpdated) {
|
||||
setIsReplicationFactorConfirmationVisible(false);
|
||||
}
|
||||
}, [replicationFactorUpdated]);
|
||||
|
||||
const partitionsSubmit = () => {
|
||||
updateTopicPartitionsCount({
|
||||
clusterName,
|
||||
topicName,
|
||||
partitions: partitionsMethods.getValues('partitions'),
|
||||
});
|
||||
};
|
||||
const replicationFactorSubmit = () => {
|
||||
updateTopicReplicationFactor({
|
||||
clusterName,
|
||||
topicName,
|
||||
replicationFactor:
|
||||
replicationFactorMethods.getValues('replicationFactor'),
|
||||
});
|
||||
};
|
||||
return (
|
||||
<S.Wrapper>
|
||||
<S.Title>Danger Zone</S.Title>
|
||||
|
@ -148,15 +133,6 @@ const DangerZone: React.FC<Props> = ({
|
|||
name="partitions"
|
||||
/>
|
||||
</FormError>
|
||||
<ConfirmationModal
|
||||
isOpen={isPartitionsConfirmationVisible}
|
||||
onCancel={() => setIsPartitionsConfirmationVisible(false)}
|
||||
onConfirm={partitionsSubmit}
|
||||
>
|
||||
Are you sure you want to increase the number of partitions? Do it only
|
||||
if you 100% know what you are doing!
|
||||
</ConfirmationModal>
|
||||
|
||||
<FormProvider {...replicationFactorMethods}>
|
||||
<S.Form
|
||||
onSubmit={replicationFactorMethods.handleSubmit(
|
||||
|
@ -170,6 +146,7 @@ const DangerZone: React.FC<Props> = ({
|
|||
</InputLabel>
|
||||
<Input
|
||||
id="replicationFactor"
|
||||
inputSize="M"
|
||||
type="number"
|
||||
placeholder="Replication Factor"
|
||||
name="replicationFactor"
|
||||
|
@ -190,20 +167,12 @@ const DangerZone: React.FC<Props> = ({
|
|||
</div>
|
||||
</S.Form>
|
||||
</FormProvider>
|
||||
|
||||
<FormError>
|
||||
<ErrorMessage
|
||||
errors={replicationFactorMethods.formState.errors}
|
||||
name="replicationFactor"
|
||||
/>
|
||||
</FormError>
|
||||
<ConfirmationModal
|
||||
isOpen={isReplicationFactorConfirmationVisible}
|
||||
onCancel={() => setIsReplicationFactorConfirmationVisible(false)}
|
||||
onConfirm={replicationFactorSubmit}
|
||||
>
|
||||
Are you sure you want to update the replication factor?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</S.Wrapper>
|
||||
);
|
||||
|
|
|
@ -4,10 +4,6 @@ import {
|
|||
updateTopicPartitionsCount,
|
||||
updateTopicReplicationFactor,
|
||||
} from 'redux/reducers/topics/topicsSlice';
|
||||
import {
|
||||
getTopicPartitionsCountIncreased,
|
||||
getTopicReplicationFactorUpdated,
|
||||
} from 'redux/reducers/topics/selectors';
|
||||
|
||||
import DangerZone from './DangerZone';
|
||||
|
||||
|
@ -17,13 +13,11 @@ type OwnProps = {
|
|||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
_: RootState,
|
||||
{ defaultPartitions, defaultReplicationFactor }: OwnProps
|
||||
) => ({
|
||||
defaultPartitions,
|
||||
defaultReplicationFactor,
|
||||
partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
|
||||
replicationFactorUpdated: getTopicReplicationFactorUpdated(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
|
|
@ -20,8 +20,6 @@ const renderComponent = (props?: Partial<Props>) =>
|
|||
<DangerZone
|
||||
defaultPartitions={defaultPartitions}
|
||||
defaultReplicationFactor={defaultReplicationFactor}
|
||||
partitionsCountIncreased={false}
|
||||
replicationFactorUpdated={false}
|
||||
updateTopicPartitionsCount={jest.fn()}
|
||||
updateTopicReplicationFactor={jest.fn()}
|
||||
{...props}
|
||||
|
@ -33,7 +31,7 @@ const renderComponent = (props?: Partial<Props>) =>
|
|||
const clickOnDialogSubmitButton = () => {
|
||||
userEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', {
|
||||
name: 'Submit',
|
||||
name: 'Confirm',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -41,7 +39,7 @@ const clickOnDialogSubmitButton = () => {
|
|||
const checkDialogThenPressCancel = async () => {
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
userEvent.click(within(dialog).getByText(/cancel/i));
|
||||
userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' }));
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
);
|
||||
|
@ -174,73 +172,6 @@ describe('DangerZone', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should close any popup if the partitionsCount is Increased ', async () => {
|
||||
renderComponent({ partitionsCountIncreased: true });
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should close any popup if the replicationFactor is Updated', async () => {
|
||||
renderComponent({ replicationFactorUpdated: true });
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should already opened Confirmation popup if partitionsCount is Increased', async () => {
|
||||
const { rerender } = renderComponent();
|
||||
const partitionInput = screen.getByPlaceholderText('Number of partitions');
|
||||
const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0];
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(partitionInput, '5');
|
||||
});
|
||||
|
||||
userEvent.click(partitionInputSubmitBtn);
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
|
||||
rerender(
|
||||
<DangerZone
|
||||
defaultPartitions={defaultPartitions}
|
||||
defaultReplicationFactor={defaultReplicationFactor}
|
||||
partitionsCountIncreased
|
||||
replicationFactorUpdated={false}
|
||||
updateTopicPartitionsCount={jest.fn()}
|
||||
updateTopicReplicationFactor={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should already opened Confirmation popup if replicationFactor is Increased', async () => {
|
||||
const { rerender } = renderComponent();
|
||||
const replicatorFactorInput =
|
||||
screen.getByPlaceholderText('Replication Factor');
|
||||
const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1];
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(replicatorFactorInput, '5');
|
||||
});
|
||||
|
||||
userEvent.click(replicatorFactorInputSubmitBtn);
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
|
||||
rerender(
|
||||
<DangerZone
|
||||
defaultPartitions={defaultPartitions}
|
||||
defaultReplicationFactor={defaultReplicationFactor}
|
||||
partitionsCountIncreased={false}
|
||||
replicationFactorUpdated
|
||||
updateTopicPartitionsCount={jest.fn()}
|
||||
updateTopicReplicationFactor={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should close the partitions dialog if he cancel button is pressed', async () => {
|
||||
renderComponent();
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ jest.mock('react-router-dom', () => ({
|
|||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
const renderComponent = async (
|
||||
props: Partial<Props> = {},
|
||||
topic: TopicWithDetailedInfo | null = topicWithInfo
|
||||
) => {
|
||||
|
@ -27,7 +27,7 @@ const renderComponent = (
|
|||
topics = getTopicStateFixtures([topic]);
|
||||
}
|
||||
|
||||
return render(
|
||||
render(
|
||||
<WithRoute path={clusterTopicEditPath()}>
|
||||
<Edit
|
||||
isFetched
|
||||
|
@ -47,8 +47,8 @@ const renderComponent = (
|
|||
describe('Edit Component', () => {
|
||||
afterEach(() => {});
|
||||
|
||||
it('renders the Edit Component', () => {
|
||||
renderComponent();
|
||||
it('renders the Edit Component', async () => {
|
||||
await act(() => renderComponent());
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: `Edit ${topicName}` })
|
||||
|
@ -58,8 +58,10 @@ describe('Edit Component', () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should check Edit component renders null is not rendered when topic is not passed', () => {
|
||||
renderComponent({}, { ...topicWithInfo, config: undefined });
|
||||
it('should check Edit component renders null is not rendered when topic is not passed', async () => {
|
||||
await act(() =>
|
||||
renderComponent({}, { ...topicWithInfo, config: undefined })
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: `Edit ${topicName}` })
|
||||
).not.toBeInTheDocument();
|
||||
|
@ -68,8 +70,8 @@ describe('Edit Component', () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should check Edit component renders null is not isFetched is false', () => {
|
||||
renderComponent({ isFetched: false });
|
||||
it('should check Edit component renders null is not isFetched is false', async () => {
|
||||
await act(() => renderComponent({ isFetched: false }));
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: `Edit ${topicName}` })
|
||||
).not.toBeInTheDocument();
|
||||
|
@ -78,10 +80,10 @@ describe('Edit Component', () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should check Edit component renders null is not topic config is not passed is false', () => {
|
||||
it('should check Edit component renders null is not topic config is not passed is false', async () => {
|
||||
const modifiedTopic = { ...topicWithInfo };
|
||||
modifiedTopic.config = undefined;
|
||||
renderComponent({}, modifiedTopic);
|
||||
await act(() => renderComponent({}, modifiedTopic));
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: `Edit ${topicName}` })
|
||||
).not.toBeInTheDocument();
|
||||
|
@ -92,7 +94,9 @@ describe('Edit Component', () => {
|
|||
|
||||
describe('Edit Component with its topic default and modified values', () => {
|
||||
it('should check the default partitions value in the DangerZone', async () => {
|
||||
renderComponent({}, { ...topicWithInfo, partitionCount: 0 });
|
||||
await act(() =>
|
||||
renderComponent({}, { ...topicWithInfo, partitionCount: 0 })
|
||||
);
|
||||
// cause topic selector will return falsy
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: `Edit ${topicName}` })
|
||||
|
@ -103,7 +107,9 @@ describe('Edit Component', () => {
|
|||
});
|
||||
|
||||
it('should check the default partitions value in the DangerZone', async () => {
|
||||
renderComponent({}, { ...topicWithInfo, replicationFactor: undefined });
|
||||
await act(() =>
|
||||
renderComponent({}, { ...topicWithInfo, replicationFactor: undefined })
|
||||
);
|
||||
expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue(
|
||||
DEFAULTS.replicationFactor
|
||||
);
|
||||
|
@ -114,7 +120,9 @@ describe('Edit Component', () => {
|
|||
it('should check the submit functionality when topic updated is false', async () => {
|
||||
const updateTopicMock = jest.fn();
|
||||
|
||||
renderComponent({ updateTopic: updateTopicMock }, undefined);
|
||||
await act(() =>
|
||||
renderComponent({ updateTopic: updateTopicMock }, undefined)
|
||||
);
|
||||
|
||||
const btn = screen.getAllByText(/Save/i)[0];
|
||||
|
||||
|
@ -134,10 +142,11 @@ describe('Edit Component', () => {
|
|||
|
||||
it('should check the submit functionality when topic updated is true', async () => {
|
||||
const updateTopicMock = jest.fn();
|
||||
|
||||
renderComponent(
|
||||
{ updateTopic: updateTopicMock, isTopicUpdated: true },
|
||||
undefined
|
||||
await act(() =>
|
||||
renderComponent(
|
||||
{ updateTopic: updateTopicMock, isTopicUpdated: true },
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
||||
const btn = screen.getAllByText(/Save/i)[0];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
|
||||
import { TopicName } from 'redux/interfaces';
|
||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import Select, { SelectOption } from 'components/common/Select/Select';
|
||||
import Input from 'components/common/Input/Input';
|
||||
|
@ -9,6 +9,9 @@ import { Button } from 'components/common/Button/Button';
|
|||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||
import { FormError } from 'components/common/Input/Input.styled';
|
||||
import { StyledForm } from 'components/common/Form/Form.styled';
|
||||
import { clusterTopicsPath } from 'lib/paths';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
|
||||
import CustomParams from './CustomParams/CustomParams';
|
||||
import TimeToRetain from './TimeToRetain';
|
||||
|
@ -54,7 +57,10 @@ const TopicForm: React.FC<Props> = ({
|
|||
const {
|
||||
control,
|
||||
formState: { errors, isDirty, isValid },
|
||||
reset,
|
||||
} = useFormContext();
|
||||
const navigate = useNavigate();
|
||||
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
|
||||
const getCleanUpPolicy =
|
||||
CleanupPolicyOptions.find((option: SelectOption) => {
|
||||
return (
|
||||
|
@ -68,6 +74,11 @@ const TopicForm: React.FC<Props> = ({
|
|||
return option.value === retentionBytes;
|
||||
})?.value || RetentionBytesOptions[0].value;
|
||||
|
||||
const onCancel = () => {
|
||||
reset();
|
||||
navigate(clusterTopicsPath(clusterName));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledForm onSubmit={onSubmit} aria-label="topic form">
|
||||
<fieldset disabled={isSubmitting}>
|
||||
|
@ -228,7 +239,12 @@ const TopicForm: React.FC<Props> = ({
|
|||
>
|
||||
{isEditing ? 'Save' : 'Create topic'}
|
||||
</Button>
|
||||
<Button type="button" buttonType="primary" buttonSize="L">
|
||||
<Button
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
buttonSize="L"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</S.ButtonWrapper>
|
||||
|
|
|
@ -4,6 +4,7 @@ import styled from 'styled-components';
|
|||
export const Alert = styled.div<{ $type: ToastType }>`
|
||||
background-color: ${({ $type, theme }) => theme.alert.color[$type]};
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
min-height: 64px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
|
|
|
@ -1,59 +1,64 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const ConfirmationModalWrapper = styled.div.attrs({ role: 'dialog' })(
|
||||
({ theme }) => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
export const Wrapper = styled.div.attrs({ role: 'dialog' })`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
export const Overlay = styled.div(
|
||||
({ theme: { modal } }) => css`
|
||||
background-color: ${modal.overlay};
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
& div:first-child {
|
||||
background-color: ${theme.modal.overlay};
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& div:last-child {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 560px;
|
||||
border-radius: 8px;
|
||||
|
||||
background-color: ${theme.modal.backgroundColor};
|
||||
filter: drop-shadow(0px 4px 16px ${theme.modal.shadow});
|
||||
|
||||
& > * {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > header {
|
||||
height: 64px;
|
||||
font-size: 20px;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
& > section {
|
||||
border-top: 1px solid ${theme.modal.border.top};
|
||||
border-bottom: 1px solid ${theme.modal.border.bottom};
|
||||
}
|
||||
|
||||
& > footer {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const Modal = styled.div(
|
||||
({ theme: { modal } }) => css`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 560px;
|
||||
border-radius: 8px;
|
||||
|
||||
background-color: ${modal.backgroundColor};
|
||||
filter: drop-shadow(0px 4px 16px ${modal.shadow});
|
||||
`
|
||||
);
|
||||
|
||||
export const Header = styled.div`
|
||||
font-size: 20px;
|
||||
text-align: start;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Content = styled.div(
|
||||
({ theme: { modal } }) => css`
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${modal.border.top};
|
||||
border-bottom: 1px solid ${modal.border.bottom};
|
||||
`
|
||||
);
|
||||
|
||||
export const Footer = styled.div`
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
|
|
@ -1,64 +1,42 @@
|
|||
import React, { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import { ConfirmContext } from 'components/contexts/ConfirmContext';
|
||||
|
||||
import { ConfirmationModalWrapper } from './ConfirmationModal.styled';
|
||||
import * as S from './ConfirmationModal.styled';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
isOpen?: boolean;
|
||||
title?: React.ReactNode;
|
||||
onConfirm(): void;
|
||||
onCancel(): void;
|
||||
isConfirming?: boolean;
|
||||
submitBtnText?: string;
|
||||
}
|
||||
const ConfirmationModal: React.FC = () => {
|
||||
const context = React.useContext(ConfirmContext);
|
||||
const isOpen = context?.content && context?.confirm;
|
||||
|
||||
const ConfirmationModal: React.FC<
|
||||
PropsWithChildren<ConfirmationModalProps>
|
||||
> = ({
|
||||
isOpen,
|
||||
children,
|
||||
title = 'Confirm the action',
|
||||
onCancel,
|
||||
onConfirm,
|
||||
isConfirming = false,
|
||||
submitBtnText = 'Submit',
|
||||
}) => {
|
||||
const cancelHandler = () => {
|
||||
if (!isConfirming) onCancel();
|
||||
};
|
||||
if (!isOpen) return null;
|
||||
|
||||
return isOpen ? (
|
||||
<ConfirmationModalWrapper>
|
||||
<div onClick={cancelHandler} aria-hidden="true" role="button" />
|
||||
<div>
|
||||
<header>
|
||||
<p>{title}</p>
|
||||
</header>
|
||||
<section>{children}</section>
|
||||
<footer>
|
||||
return (
|
||||
<S.Wrapper role="dialog" aria-label="Confirmation Dialog">
|
||||
<S.Overlay onClick={context.cancel} aria-hidden="true" role="button" />
|
||||
<S.Modal>
|
||||
<S.Header>Confirm the action</S.Header>
|
||||
<S.Content>{context.content}</S.Content>
|
||||
<S.Footer>
|
||||
<Button
|
||||
buttonType="secondary"
|
||||
buttonSize="M"
|
||||
onClick={cancelHandler}
|
||||
onClick={context.cancel}
|
||||
type="button"
|
||||
disabled={isConfirming}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="M"
|
||||
onClick={onConfirm}
|
||||
onClick={context.confirm}
|
||||
type="button"
|
||||
disabled={isConfirming}
|
||||
>
|
||||
{submitBtnText}
|
||||
Confirm
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ConfirmationModalWrapper>
|
||||
) : null;
|
||||
</S.Footer>
|
||||
</S.Modal>
|
||||
</S.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationModal;
|
||||
|
|
|
@ -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;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
export const DangerItem = styled.div`
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import React, { PropsWithChildren } from 'react';
|
||||
import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
|
||||
import * as S from './Dropdown.styled';
|
||||
|
||||
interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
|
||||
danger?: boolean;
|
||||
onClick?(): void;
|
||||
confirm?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownItem: React.FC<DropdownItemProps> = ({
|
||||
onClick,
|
||||
danger,
|
||||
children,
|
||||
confirm,
|
||||
...rest
|
||||
}) => {
|
||||
const confirmation = useConfirm();
|
||||
|
||||
const handleClick = (e: ClickEvent) => {
|
||||
if (!onClick) return;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
e.stopPropagation = true;
|
||||
e.syntheticEvent.stopPropagation();
|
||||
onClick();
|
||||
|
||||
if (confirm) {
|
||||
confirmation(confirm, onClick);
|
||||
} else {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 { render } from 'lib/testHelpers';
|
||||
import useDataSaver from 'lib/hooks/useDataSaver';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('useDataSaver hook', () => {
|
||||
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,
|
||||
UseQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
import { ConfirmContextProvider } from 'components/contexts/ConfirmContext';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
|
||||
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||
preloadedState?: Partial<RootState>;
|
||||
|
@ -67,13 +69,18 @@ const customRender = (
|
|||
children,
|
||||
}) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<TestQueryClientProvider>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
{children}
|
||||
</MemoryRouter>
|
||||
</TestQueryClientProvider>
|
||||
</Provider>
|
||||
<ConfirmContextProvider>
|
||||
<Provider store={store}>
|
||||
<TestQueryClientProvider>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<div>
|
||||
{children}
|
||||
<ConfirmationModal />
|
||||
</div>
|
||||
</MemoryRouter>
|
||||
</TestQueryClientProvider>
|
||||
</Provider>
|
||||
</ConfirmContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
return render(ui, { wrapper: AllTheProviders, ...renderOptions });
|
||||
|
|
|
@ -27,7 +27,7 @@ export const clearTopicMessages = createAsyncThunk<
|
|||
dispatch(fetchTopicDetails({ clusterName, topicName }));
|
||||
showSuccessAlert({
|
||||
id: `message-${topicName}-${clusterName}-${partitions}`,
|
||||
message: 'Messages successfully cleared!',
|
||||
message: `${topicName} messages have been successfully cleared!`,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -20,12 +20,6 @@ describe('Topics selectors', () => {
|
|||
expect(selectors.getTopicMessageSchemaFetched(store.getState())).toEqual(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
selectors.getTopicPartitionsCountIncreased(store.getState())
|
||||
).toEqual(false);
|
||||
expect(
|
||||
selectors.getTopicReplicationFactorUpdated(store.getState())
|
||||
).toEqual(false);
|
||||
expect(
|
||||
selectors.getTopicsConsumerGroupsFetched(store.getState())
|
||||
).toEqual(false);
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
fetchTopicConsumerGroups,
|
||||
createTopic,
|
||||
deleteTopic,
|
||||
updateTopicPartitionsCount,
|
||||
updateTopicReplicationFactor,
|
||||
} from 'redux/reducers/topics/topicsSlice';
|
||||
import { AsyncRequestStatus } from 'lib/constants';
|
||||
|
||||
|
@ -90,24 +88,6 @@ export const getTopicMessageSchemaFetched = createSelector(
|
|||
(status) => status === AsyncRequestStatus.fulfilled
|
||||
);
|
||||
|
||||
const getPartitionsCountIncreaseStatus = createFetchingSelector(
|
||||
updateTopicPartitionsCount.typePrefix
|
||||
);
|
||||
|
||||
export const getTopicPartitionsCountIncreased = createSelector(
|
||||
getPartitionsCountIncreaseStatus,
|
||||
(status) => status === AsyncRequestStatus.fulfilled
|
||||
);
|
||||
|
||||
const getReplicationFactorUpdateStatus = createFetchingSelector(
|
||||
updateTopicReplicationFactor.typePrefix
|
||||
);
|
||||
|
||||
export const getTopicReplicationFactorUpdated = createSelector(
|
||||
getReplicationFactorUpdateStatus,
|
||||
(status) => status === AsyncRequestStatus.fulfilled
|
||||
);
|
||||
|
||||
const getTopicConsumerGroupsStatus = createFetchingSelector(
|
||||
fetchTopicConsumerGroups.typePrefix
|
||||
);
|
||||
|
|
|
@ -274,7 +274,7 @@ export const updateTopicPartitionsCount = createAsyncThunk<
|
|||
});
|
||||
showSuccessAlert({
|
||||
id: `message-${topicName}-${clusterName}-${partitions}`,
|
||||
message: 'Number of partitions successfully increased!',
|
||||
message: 'Number of partitions successfully increased',
|
||||
});
|
||||
dispatch(fetchTopicDetails({ clusterName, topicName }));
|
||||
return undefined;
|
||||
|
@ -294,7 +294,7 @@ export const updateTopicReplicationFactor = createAsyncThunk<
|
|||
}
|
||||
>(
|
||||
'topic/updateTopicReplicationFactor',
|
||||
async (payload, { rejectWithValue }) => {
|
||||
async (payload, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { clusterName, topicName, replicationFactor } = payload;
|
||||
|
||||
|
@ -303,7 +303,11 @@ export const updateTopicReplicationFactor = createAsyncThunk<
|
|||
topicName,
|
||||
replicationFactorChange: { totalReplicationFactor: replicationFactor },
|
||||
});
|
||||
|
||||
showSuccessAlert({
|
||||
id: `message-${topicName}-${clusterName}-replicationFactor`,
|
||||
message: 'Replication Factor successfully updated',
|
||||
});
|
||||
dispatch(fetchTopicDetails({ clusterName, topicName }));
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
showServerError(err as Response);
|
||||
|
@ -340,22 +344,24 @@ export const clearTopicsMessages = createAsyncThunk<
|
|||
clusterName: ClusterName;
|
||||
topicNames: TopicName[];
|
||||
}
|
||||
>('topic/clearTopicsMessages', async (payload, { rejectWithValue }) => {
|
||||
try {
|
||||
const { clusterName, topicNames } = payload;
|
||||
>(
|
||||
'topic/clearTopicsMessages',
|
||||
async (payload, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { clusterName, topicNames } = payload;
|
||||
topicNames.forEach((topicName) => {
|
||||
dispatch(clearTopicMessages({ clusterName, topicName }));
|
||||
});
|
||||
|
||||
topicNames.forEach((topicName) => {
|
||||
clearTopicMessages({ clusterName, topicName });
|
||||
});
|
||||
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
showServerError(err as Response);
|
||||
return rejectWithValue(await getResponse(err as Response));
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
showServerError(err as Response);
|
||||
return rejectWithValue(await getResponse(err as Response));
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const initialState: TopicsState = {
|
||||
const initialState: TopicsState = {
|
||||
byName: {},
|
||||
allNames: [],
|
||||
totalPages: 1,
|
||||
|
|
Loading…
Add table
Reference in a new issue