UI for issues/243 and issues/244
This commit is contained in:
parent
4067880966
commit
08a3c5e225
5 changed files with 279 additions and 2 deletions
|
@ -12,6 +12,7 @@ import usePagination from 'lib/hooks/usePagination';
|
|||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import Pagination from 'components/common/Pagination/Pagination';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { GetTopicsRequest, TopicColumnsToSort } from 'generated-sources';
|
||||
import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
|
||||
import Search from 'components/common/Search/Search';
|
||||
|
@ -25,6 +26,8 @@ export interface TopicsListProps {
|
|||
totalPages: number;
|
||||
fetchTopicsList(props: GetTopicsRequest): void;
|
||||
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
|
||||
deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
|
||||
clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
|
||||
clearTopicMessages(
|
||||
topicName: TopicName,
|
||||
clusterName: ClusterName,
|
||||
|
@ -42,7 +45,9 @@ const List: React.FC<TopicsListProps> = ({
|
|||
totalPages,
|
||||
fetchTopicsList,
|
||||
deleteTopic,
|
||||
deleteTopics,
|
||||
clearTopicMessages,
|
||||
clearTopicsMessages,
|
||||
search,
|
||||
orderBy,
|
||||
setTopicsSearch,
|
||||
|
@ -78,6 +83,45 @@ const List: React.FC<TopicsListProps> = ({
|
|||
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
|
||||
}, [showInternal]);
|
||||
|
||||
const [confirmationModal, setConfirmationModal] = React.useState<
|
||||
'' | 'deleteTopics' | 'purgeMessages'
|
||||
>('');
|
||||
|
||||
const closeConfirmationModal = () => {
|
||||
setConfirmationModal('');
|
||||
};
|
||||
|
||||
const [selectedTopics, setSelectedTopics] = React.useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const clearSelectedTopics = () => {
|
||||
setSelectedTopics(new Set());
|
||||
};
|
||||
|
||||
const toggleTopicSelected = (topicName: string) => {
|
||||
setSelectedTopics((prevState) => {
|
||||
const newState = new Set(prevState);
|
||||
if (newState.has(topicName)) {
|
||||
newState.delete(topicName);
|
||||
} else {
|
||||
newState.add(topicName);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteTopicsHandler = React.useCallback(() => {
|
||||
deleteTopics(clusterName, Array.from(selectedTopics));
|
||||
closeConfirmationModal();
|
||||
clearSelectedTopics();
|
||||
}, [clusterName, selectedTopics]);
|
||||
const purgeMessagesHandler = React.useCallback(() => {
|
||||
clearTopicsMessages(clusterName, Array.from(selectedTopics));
|
||||
closeConfirmationModal();
|
||||
clearSelectedTopics();
|
||||
}, [clusterName, selectedTopics]);
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
|
||||
|
@ -119,9 +163,47 @@ const List: React.FC<TopicsListProps> = ({
|
|||
<PageLoader />
|
||||
) : (
|
||||
<div className="box">
|
||||
{selectedTopics.size > 0 && (
|
||||
<>
|
||||
<div className="buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="button is-danger"
|
||||
onClick={() => {
|
||||
setConfirmationModal('deleteTopics');
|
||||
}}
|
||||
>
|
||||
Delete selected topics
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button is-danger"
|
||||
onClick={() => {
|
||||
setConfirmationModal('purgeMessages');
|
||||
}}
|
||||
>
|
||||
Purge messages of selected topics
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={confirmationModal !== ''}
|
||||
onCancel={closeConfirmationModal}
|
||||
onConfirm={
|
||||
confirmationModal === 'deleteTopics'
|
||||
? deleteTopicsHandler
|
||||
: purgeMessagesHandler
|
||||
}
|
||||
>
|
||||
{confirmationModal === 'deleteTopics'
|
||||
? 'Are you sure want to remove selected topics?'
|
||||
: 'Are you sure want to purge messages of selected topics?'}
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)}
|
||||
<table className="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<SortableColumnHeader
|
||||
value={TopicColumnsToSort.NAME}
|
||||
title="Topic Name"
|
||||
|
@ -154,6 +236,8 @@ const List: React.FC<TopicsListProps> = ({
|
|||
clusterName={clusterName}
|
||||
key={topic.name}
|
||||
topic={topic}
|
||||
selected={selectedTopics.has(topic.name)}
|
||||
toggleTopicSelected={toggleTopicSelected}
|
||||
deleteTopic={deleteTopic}
|
||||
clearTopicMessages={clearTopicMessages}
|
||||
/>
|
||||
|
|
|
@ -14,6 +14,8 @@ import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
|||
|
||||
export interface ListItemProps {
|
||||
topic: TopicWithDetailedInfo;
|
||||
selected: boolean;
|
||||
toggleTopicSelected(topicName: TopicName): void;
|
||||
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||
clusterName: ClusterName;
|
||||
clearTopicMessages(topicName: TopicName, clusterName: ClusterName): void;
|
||||
|
@ -28,6 +30,8 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
replicationFactor,
|
||||
cleanUpPolicy,
|
||||
},
|
||||
selected,
|
||||
toggleTopicSelected,
|
||||
deleteTopic,
|
||||
clusterName,
|
||||
clearTopicMessages,
|
||||
|
@ -70,6 +74,17 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{!internal && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={() => {
|
||||
toggleTopicSelected(name);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="has-text-overflow-ellipsis">
|
||||
<NavLink
|
||||
exact
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { Route, Router } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import ClusterContext, {
|
||||
ContextProps,
|
||||
} from 'components/contexts/ClusterContext';
|
||||
|
@ -8,6 +9,13 @@ import List, { TopicsListProps } from 'components/Topics/List/List';
|
|||
import { createMemoryHistory } from 'history';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import Search from 'components/common/Search/Search';
|
||||
import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
|
||||
import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
|
||||
jest.mock(
|
||||
'components/common/ConfirmationModal/ConfirmationModal',
|
||||
() => 'mock-ConfirmationModal'
|
||||
);
|
||||
|
||||
describe('List', () => {
|
||||
const setupComponent = (props: Partial<TopicsListProps> = {}) => (
|
||||
|
@ -17,6 +25,8 @@ describe('List', () => {
|
|||
totalPages={1}
|
||||
fetchTopicsList={jest.fn()}
|
||||
deleteTopic={jest.fn()}
|
||||
deleteTopics={jest.fn()}
|
||||
clearTopicsMessages={jest.fn()}
|
||||
clearTopicMessages={jest.fn()}
|
||||
search=""
|
||||
orderBy={null}
|
||||
|
@ -123,4 +133,130 @@ describe('List', () => {
|
|||
expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when some list items are selected', () => {
|
||||
const mockDeleteTopics = jest.fn();
|
||||
const mockClearTopicsMessages = jest.fn();
|
||||
jest.useFakeTimers();
|
||||
const pathname = '/ui/clusters/local/topics';
|
||||
const component = mount(
|
||||
<StaticRouter location={{ pathname }}>
|
||||
<Route path="/ui/clusters/:clusterName">
|
||||
<ClusterContext.Provider
|
||||
value={{
|
||||
isReadOnly: false,
|
||||
hasKafkaConnectConfigured: true,
|
||||
hasSchemaRegistryConfigured: true,
|
||||
}}
|
||||
>
|
||||
{setupComponent({
|
||||
topics: [
|
||||
externalTopicPayload,
|
||||
{ ...externalTopicPayload, name: 'external.topic2' },
|
||||
],
|
||||
deleteTopics: mockDeleteTopics,
|
||||
clearTopicsMessages: mockClearTopicsMessages,
|
||||
})}
|
||||
</ClusterContext.Provider>
|
||||
</Route>
|
||||
</StaticRouter>
|
||||
);
|
||||
const getCheckboxInput = (at: number) =>
|
||||
component.find('ListItem').at(at).find('input[type="checkbox"]').at(0);
|
||||
|
||||
const getConfirmationModal = () =>
|
||||
component.find('mock-ConfirmationModal').at(0);
|
||||
|
||||
it('renders delete/purge buttons', () => {
|
||||
expect(getCheckboxInput(0).props().checked).toBeFalsy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeFalsy();
|
||||
expect(component.find('.buttons').length).toEqual(0);
|
||||
|
||||
// check first item
|
||||
getCheckboxInput(0).simulate('change');
|
||||
expect(getCheckboxInput(0).props().checked).toBeTruthy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeFalsy();
|
||||
expect(component.find('.buttons').length).toEqual(1);
|
||||
|
||||
// check second item
|
||||
getCheckboxInput(1).simulate('change');
|
||||
expect(getCheckboxInput(0).props().checked).toBeTruthy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeTruthy();
|
||||
expect(component.find('.buttons').length).toEqual(1);
|
||||
|
||||
// uncheck second item
|
||||
getCheckboxInput(1).simulate('change');
|
||||
expect(getCheckboxInput(0).props().checked).toBeTruthy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeFalsy();
|
||||
expect(component.find('.buttons').length).toEqual(1);
|
||||
|
||||
// uncheck first item
|
||||
getCheckboxInput(0).simulate('change');
|
||||
expect(getCheckboxInput(0).props().checked).toBeFalsy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeFalsy();
|
||||
expect(component.find('.buttons').length).toEqual(0);
|
||||
});
|
||||
|
||||
const checkActionButtonClick = async (action: string) => {
|
||||
const buttonIndex = action === 'deleteTopics' ? 0 : 1;
|
||||
const confirmationText =
|
||||
action === 'deleteTopics'
|
||||
? 'Are you sure want to remove selected topics?'
|
||||
: 'Are you sure want to purge messages of selected topics?';
|
||||
const mockFn =
|
||||
action === 'deleteTopics' ? mockDeleteTopics : mockClearTopicsMessages;
|
||||
getCheckboxInput(0).simulate('change');
|
||||
getCheckboxInput(1).simulate('change');
|
||||
let modal = getConfirmationModal();
|
||||
expect(modal.prop('isOpen')).toBeFalsy();
|
||||
component
|
||||
.find('.buttons')
|
||||
.find('button')
|
||||
.at(buttonIndex)
|
||||
.simulate('click');
|
||||
expect(modal.text()).toEqual(confirmationText);
|
||||
modal = getConfirmationModal();
|
||||
expect(modal.prop('isOpen')).toBeTruthy();
|
||||
await act(async () => {
|
||||
(modal.props() as ConfirmationModalProps).onConfirm();
|
||||
});
|
||||
component.update();
|
||||
expect(getConfirmationModal().prop('isOpen')).toBeFalsy();
|
||||
expect(getCheckboxInput(0).props().checked).toBeFalsy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeFalsy();
|
||||
expect(component.find('.buttons').length).toEqual(0);
|
||||
expect(mockFn).toBeCalledTimes(1);
|
||||
expect(mockFn).toBeCalledWith('local', [
|
||||
externalTopicPayload.name,
|
||||
'external.topic2',
|
||||
]);
|
||||
};
|
||||
|
||||
it('triggers the deleteTopics when clicked on the delete button', async () => {
|
||||
await checkActionButtonClick('deleteTopics');
|
||||
});
|
||||
|
||||
it('triggers the clearTopicsMessages when clicked on the clear button', async () => {
|
||||
await checkActionButtonClick('clearTopicsMessages');
|
||||
});
|
||||
|
||||
it('closes ConfirmationModal when clicked on the cancel button', async () => {
|
||||
getCheckboxInput(0).simulate('change');
|
||||
getCheckboxInput(1).simulate('change');
|
||||
let modal = getConfirmationModal();
|
||||
expect(modal.prop('isOpen')).toBeFalsy();
|
||||
component.find('.buttons').find('button').at(0).simulate('click');
|
||||
modal = getConfirmationModal();
|
||||
expect(modal.prop('isOpen')).toBeTruthy();
|
||||
await act(async () => {
|
||||
(modal.props() as ConfirmationModalProps).onCancel();
|
||||
});
|
||||
component.update();
|
||||
expect(getConfirmationModal().prop('isOpen')).toBeFalsy();
|
||||
expect(getCheckboxInput(0).props().checked).toBeTruthy();
|
||||
expect(getCheckboxInput(1).props().checked).toBeTruthy();
|
||||
expect(component.find('.buttons').length).toEqual(1);
|
||||
expect(mockDeleteTopics).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import ListItem, { ListItemProps } from 'components/Topics/List/ListItem';
|
|||
const mockDelete = jest.fn();
|
||||
const clusterName = 'local';
|
||||
const mockDeleteMessages = jest.fn();
|
||||
const mockToggleTopicSelected = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
'components/common/ConfirmationModal/ConfirmationModal',
|
||||
|
@ -23,6 +24,8 @@ describe('ListItem', () => {
|
|||
deleteTopic={mockDelete}
|
||||
clusterName={clusterName}
|
||||
clearTopicMessages={mockDeleteMessages}
|
||||
selected={false}
|
||||
toggleTopicSelected={mockToggleTopicSelected}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -73,6 +76,18 @@ describe('ListItem', () => {
|
|||
expect(wrapper.find('.tag.is-light').text()).toEqual('Internal');
|
||||
});
|
||||
|
||||
it('renders without checkbox for internal topic', () => {
|
||||
const wrapper = mount(
|
||||
<StaticRouter>
|
||||
<table>
|
||||
<tbody>{setupComponent()}</tbody>
|
||||
</table>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
expect(wrapper.find('td').at(0).html()).toEqual('<td></td>');
|
||||
});
|
||||
|
||||
it('renders correct tags for external topic', () => {
|
||||
const wrapper = mount(
|
||||
<StaticRouter>
|
||||
|
@ -85,6 +100,28 @@ describe('ListItem', () => {
|
|||
expect(wrapper.find('.tag.is-primary').text()).toEqual('External');
|
||||
});
|
||||
|
||||
it('renders with checkbox for external topic', () => {
|
||||
const wrapper = mount(
|
||||
<StaticRouter>
|
||||
<table>
|
||||
<tbody>{setupComponent({ topic: externalTopicPayload })}</tbody>
|
||||
</table>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
expect(wrapper.find('td').at(0).html()).toEqual(
|
||||
'<td><input type="checkbox"></td>'
|
||||
);
|
||||
});
|
||||
|
||||
it('triggers the toggleTopicSelected when clicked on the checkbox input', () => {
|
||||
const wrapper = shallow(setupComponent({ topic: externalTopicPayload }));
|
||||
expect(wrapper.exists('input')).toBeTruthy();
|
||||
wrapper.find('input[type="checkbox"]').at(0).simulate('change');
|
||||
expect(mockToggleTopicSelected).toBeCalledTimes(1);
|
||||
expect(mockToggleTopicSelected).toBeCalledWith(externalTopicPayload.name);
|
||||
});
|
||||
|
||||
it('renders correct out of sync replicas number', () => {
|
||||
const wrapper = mount(
|
||||
<StaticRouter>
|
||||
|
@ -98,6 +135,6 @@ describe('ListItem', () => {
|
|||
</StaticRouter>
|
||||
);
|
||||
|
||||
expect(wrapper.find('td').at(2).text()).toEqual('0');
|
||||
expect(wrapper.find('td').at(3).text()).toEqual('0');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,9 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
|
|||
<List
|
||||
areTopicsFetching={false}
|
||||
clearTopicMessages={[MockFunction]}
|
||||
clearTopicsMessages={[MockFunction]}
|
||||
deleteTopic={[MockFunction]}
|
||||
deleteTopics={[MockFunction]}
|
||||
fetchTopicsList={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -165,6 +167,9 @@ exports[`List when it does not have readonly flag matches the snapshot 1`] = `
|
|||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
<ListHeaderCell
|
||||
orderBy={null}
|
||||
setOrderBy={[MockFunction]}
|
||||
|
|
Loading…
Add table
Reference in a new issue