[Issue-998] Add Recreate topic button in to list of Topic and Details topic Overview (#1660)

* Add Recreate topic button in to list of Topic and Details topic Overview

* Add reducer and update test

* update reducer test

* update Topic/Details test

* Table and TableColumn components, TableState and DataSource

* Table: Migrate topics table to new Table component

* fix module paths

* test for propertyLookup

* improve useTableState code

* fix folder name

* improve table ordering

Co-authored-by: Anton Zorin <zorii4@Antons-MacBook-Pro.local>
Co-authored-by: Sash Stepanyan <sstepanyan@provectus.com>
Co-authored-by: Sasha Stepanyan <100123785+sasunprov@users.noreply.github.com>
This commit is contained in:
Zorii4 2022-03-21 12:58:13 +03:00 committed by GitHub
parent ad628ed7ae
commit 634406ac91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 2 deletions

View file

@ -47,6 +47,7 @@ export interface TopicsListProps {
fetchTopicsList(props: GetTopicsRequest): void;
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
recreateTopic(topicName: TopicName, clusterName: ClusterName): void;
clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
clearTopicMessages(
topicName: TopicName,
@ -67,6 +68,7 @@ const List: React.FC<TopicsListProps> = ({
fetchTopicsList,
deleteTopic,
deleteTopics,
recreateTopic,
clearTopicMessages,
clearTopicsMessages,
search,
@ -169,6 +171,11 @@ const List: React.FC<TopicsListProps> = ({
setDeleteTopicConfirmationVisible,
] = React.useState(false);
const [
isRecreateTopicConfirmationVisible,
setRecreateTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = React.useCallback(() => {
deleteTopic(clusterName, name);
}, [name]);
@ -176,6 +183,12 @@ const List: React.FC<TopicsListProps> = ({
const clearTopicMessagesHandler = React.useCallback(() => {
clearTopicMessages(clusterName, name);
}, [name]);
const recreateTopicHandler = React.useCallback(() => {
recreateTopic(clusterName, name);
setRecreateTopicConfirmationVisible(false);
}, [name]);
return (
<>
{!internal && !isReadOnly && hovered ? (
@ -194,6 +207,12 @@ const List: React.FC<TopicsListProps> = ({
Remove Topic
</DropdownItem>
)}
<DropdownItem
onClick={() => setRecreateTopicConfirmationVisible(true)}
danger
>
Recreate Topic
</DropdownItem>
</Dropdown>
</div>
) : null}
@ -204,6 +223,13 @@ const List: React.FC<TopicsListProps> = ({
>
Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal>
<ConfirmationModal
isOpen={isRecreateTopicConfirmationVisible}
onCancel={() => setRecreateTopicConfirmationVisible(false)}
onConfirm={recreateTopicHandler}
>
Are you sure to recreate <b>{name}</b> topic?
</ConfirmationModal>
</>
);
}

View file

@ -4,6 +4,7 @@ import {
fetchTopicsList,
deleteTopic,
deleteTopics,
recreateTopic,
clearTopicsMessages,
clearTopicMessages,
setTopicsSearchAction,
@ -33,6 +34,7 @@ const mapDispatchToProps = {
fetchTopicsList,
deleteTopic,
deleteTopics,
recreateTopic,
clearTopicsMessages,
clearTopicMessages,
setTopicsSearch: setTopicsSearchAction,

View file

@ -20,6 +20,7 @@ export interface ListItemProps {
selected: boolean;
toggleTopicSelected(topicName: TopicName): void;
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
recreateTopic: (clusterName: ClusterName, topicName: TopicName) => void;
clusterName: ClusterName;
clearTopicMessages(topicName: TopicName, clusterName: ClusterName): void;
}
@ -36,6 +37,7 @@ const ListItem: React.FC<ListItemProps> = ({
selected,
toggleTopicSelected,
deleteTopic,
recreateTopic,
clusterName,
clearTopicMessages,
}) => {
@ -45,6 +47,11 @@ const ListItem: React.FC<ListItemProps> = ({
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
React.useState(false);
const [
isRecreateTopicConfirmationVisible,
setRecreateTopicConfirmationVisible,
] = React.useState(false);
const { outOfSyncReplicas, numberOfMessages } = React.useMemo(() => {
if (partitions === undefined || partitions.length === 0) {
return {
@ -72,6 +79,11 @@ const ListItem: React.FC<ListItemProps> = ({
deleteTopic(clusterName, name);
}, [clusterName, deleteTopic, name]);
const recreateTopicHandler = React.useCallback(() => {
recreateTopic(clusterName, name);
setRecreateTopicConfirmationVisible(false);
}, [recreateTopic, clusterName, name]);
const clearTopicMessagesHandler = React.useCallback(() => {
clearTopicMessages(clusterName, name);
}, [clearTopicMessages, clusterName, name]);
@ -125,6 +137,12 @@ const ListItem: React.FC<ListItemProps> = ({
Remove Topic
</DropdownItem>
)}
<DropdownItem
onClick={() => setRecreateTopicConfirmationVisible(true)}
danger
>
Recreate Topic
</DropdownItem>
</Dropdown>
</div>
) : null}
@ -135,6 +153,13 @@ const ListItem: React.FC<ListItemProps> = ({
>
Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal>
<ConfirmationModal
isOpen={isRecreateTopicConfirmationVisible}
onCancel={() => setRecreateTopicConfirmationVisible(false)}
onConfirm={recreateTopicHandler}
>
Are you sure to recreate <b>{name}</b> topic?
</ConfirmationModal>
</td>
</tr>
);

View file

@ -32,6 +32,7 @@ describe('List', () => {
deleteTopics={jest.fn()}
clearTopicsMessages={jest.fn()}
clearTopicMessages={jest.fn()}
recreateTopic={jest.fn()}
search=""
orderBy={null}
sortOrder={SortOrder.ASC}

View file

@ -13,7 +13,7 @@ const mockDelete = jest.fn();
const clusterName = 'local';
const mockDeleteMessages = jest.fn();
const mockToggleTopicSelected = jest.fn();
const mockRecreateTopic = jest.fn();
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
@ -35,6 +35,7 @@ describe('ListItem', () => {
deleteTopic={mockDelete}
clusterName={clusterName}
clearTopicMessages={mockDeleteMessages}
recreateTopic={mockRecreateTopic}
selected={false}
toggleTopicSelected={mockToggleTopicSelected}
{...props}

View file

@ -36,6 +36,7 @@ interface Props extends Topic, TopicDetails {
isDeleted: boolean;
isDeletePolicy: boolean;
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
recreateTopic: (clusterName: ClusterName, topicName: TopicName) => void;
clearTopicMessages(clusterName: ClusterName, topicName: TopicName): void;
}
@ -53,6 +54,7 @@ const Details: React.FC<Props> = ({
isDeleted,
isDeletePolicy,
deleteTopic,
recreateTopic,
clearTopicMessages,
}) => {
const history = useHistory();
@ -63,6 +65,10 @@ const Details: React.FC<Props> = ({
React.useState(false);
const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
React.useState(false);
const [
isRecreateTopicConfirmationVisible,
setRecreateTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = React.useCallback(() => {
deleteTopic(clusterName, topicName);
}, [clusterName, topicName, deleteTopic]);
@ -79,6 +85,11 @@ const Details: React.FC<Props> = ({
setClearTopicConfirmationVisible(false);
}, [clusterName, topicName, clearTopicMessages]);
const recreateTopicHandler = React.useCallback(() => {
recreateTopic(clusterName, topicName);
setRecreateTopicConfirmationVisible(false);
}, [recreateTopic, clusterName, topicName]);
return (
<div>
<PageHeading text={topicName}>
@ -116,6 +127,12 @@ const Details: React.FC<Props> = ({
Clear messages
</DropdownItem>
)}
<DropdownItem
onClick={() => setRecreateTopicConfirmationVisible(true)}
danger
>
Recreate Topic
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem
onClick={() => setDeleteTopicConfirmationVisible(true)}
@ -143,6 +160,13 @@ const Details: React.FC<Props> = ({
>
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
exact

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { ClusterName, RootState, TopicName } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { deleteTopic, clearTopicMessages } from 'redux/actions';
import { deleteTopic, clearTopicMessages, recreateTopic } from 'redux/actions';
import {
getIsTopicDeleted,
getIsTopicDeletePolicy,
@ -33,6 +33,7 @@ const mapStateToProps = (
});
const mapDispatchToProps = {
recreateTopic,
deleteTopic,
clearTopicMessages,
};

View file

@ -12,6 +12,7 @@ describe('Details', () => {
const mockClusterName = 'local';
const mockClearTopicMessages = jest.fn();
const mockInternalTopicPayload = internalTopicPayload.internal;
const mockRecreateTopic = jest.fn();
const setupComponent = (pathname: string) =>
render(
@ -29,6 +30,7 @@ describe('Details', () => {
name={internalTopicPayload.name}
isInternal={false}
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
isDeletePolicy
@ -54,6 +56,7 @@ describe('Details', () => {
name={internalTopicPayload.name}
isInternal={mockInternalTopicPayload}
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
isDeletePolicy
@ -77,4 +80,40 @@ describe('Details', () => {
getByText(/Are you sure want to clear topic messages?/i)
).toBeInTheDocument();
});
it('shows a confirmation popup on recreating topic', () => {
setupComponent(
clusterTopicPath(mockClusterName, internalTopicPayload.name)
);
const recreateTopicButton = screen.getByText(/Recreate topic/i);
userEvent.click(recreateTopicButton);
expect(
screen.getByText(/Are you sure want to recreate topic?/i)
).toBeInTheDocument();
});
it('calling recreation function after click on Submit button', () => {
setupComponent(
clusterTopicPath(mockClusterName, internalTopicPayload.name)
);
const recreateTopicButton = screen.getByText(/Recreate topic/i);
userEvent.click(recreateTopicButton);
const confirmBtn = screen.getByRole('button', { name: /submit/i });
userEvent.click(confirmBtn);
expect(mockRecreateTopic).toBeCalledTimes(1);
});
it('close popup confirmation window after click on Cancel button', () => {
setupComponent(
clusterTopicPath(mockClusterName, internalTopicPayload.name)
);
const recreateTopicButton = screen.getByText(/Recreate topic/i);
userEvent.click(recreateTopicButton);
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
userEvent.click(cancelBtn);
expect(
screen.queryByText(/Are you sure want to recreate topic?/i)
).not.toBeInTheDocument();
});
});

View file

@ -6,6 +6,7 @@ import { mockTopicsState } from 'redux/actions/__test__/fixtures';
import { MessageSchemaSourceEnum, TopicMessageSchema } from 'generated-sources';
import { FailurePayload } from 'redux/interfaces';
import { getResponse } from 'lib/errorHandling';
import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
const store = mockStoreCreator;
@ -49,6 +50,37 @@ describe('Thunks', () => {
});
});
describe('recreateTopic', () => {
it('creates RECREATE_TOPIC__SUCCESS when recreating existing topic', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
internalTopicPayload
);
await store.dispatch(thunks.recreateTopic(clusterName, topicName));
expect(store.getActions()).toEqual([
actions.recreateTopicAction.request(),
actions.recreateTopicAction.success(internalTopicPayload),
]);
});
it('creates RECREATE_TOPIC__FAILURE when recreating existing topic', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
404
);
try {
await store.dispatch(thunks.recreateTopic(clusterName, topicName));
} catch (error) {
const err = error as Response;
expect(err.status).toEqual(404);
expect(store.getActions()).toEqual([
actions.recreateTopicAction.request(),
actions.recreateTopicAction.failure(),
]);
}
});
});
describe('clearTopicMessages', () => {
it('creates CLEAR_TOPIC_MESSAGES__SUCCESS when deleting existing messages', async () => {
fetchMock.deleteOnce(

View file

@ -12,6 +12,7 @@ import {
FullConnectorInfo,
Connect,
Task,
Topic,
TopicMessage,
TopicMessageConsuming,
TopicMessageSchema,
@ -60,6 +61,13 @@ export const deleteTopicAction = createAsyncAction(
'DELETE_TOPIC__CANCEL'
)<undefined, TopicName, undefined, undefined>();
export const recreateTopicAction = createAsyncAction(
'RECREATE_TOPIC__REQUEST',
'RECREATE_TOPIC__SUCCESS',
'RECREATE_TOPIC__FAILURE',
'RECREATE_TOPIC__CANCEL'
)<undefined, Topic, undefined, undefined>();
export const dismissAlert = createAction('DISMISS_ALERT')<string>();
export const fetchConnectsAction = createAsyncAction(

View file

@ -259,6 +259,21 @@ export const deleteTopic =
}
};
export const recreateTopic =
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
async (dispatch) => {
dispatch(actions.recreateTopicAction.request());
try {
const topic = await topicsApiClient.recreateTopic({
clusterName,
topicName,
});
dispatch(actions.recreateTopicAction.success(topic));
} catch (e) {
dispatch(actions.recreateTopicAction.failure());
}
};
export const deleteTopics =
(clusterName: ClusterName, topicsName: TopicName[]): PromiseThunkResult =>
async (dispatch) => {

View file

@ -10,6 +10,7 @@ import {
setTopicsOrderByAction,
fetchTopicConsumerGroupsAction,
fetchTopicMessageSchemaAction,
recreateTopicAction,
} from 'redux/actions';
import reducer from 'redux/reducers/topics/reducer';
@ -94,6 +95,15 @@ describe('topics reducer', () => {
it('delete topic messages on CLEAR_TOPIC_MESSAGES__SUCCESS', () => {
expect(reducer(state, clearMessagesTopicAction.success())).toEqual(state);
});
it('recreate topic', () => {
expect(reducer(state, recreateTopicAction.success(topic))).toEqual({
...state,
byName: {
[topic.name]: topic,
},
});
});
});
describe('search topics', () => {

View file

@ -32,6 +32,14 @@ const reducer = (state = initialState, action: Action): TopicsState => {
);
return newState;
}
case getType(actions.recreateTopicAction.success):
return {
...state,
byName: {
...state.byName,
[action.payload.name]: { ...action.payload },
},
};
case getType(actions.setTopicsSearchAction): {
return {
...state,