[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:
parent
ad628ed7ae
commit
634406ac91
13 changed files with 194 additions and 2 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@ describe('List', () => {
|
|||
deleteTopics={jest.fn()}
|
||||
clearTopicsMessages={jest.fn()}
|
||||
clearTopicMessages={jest.fn()}
|
||||
recreateTopic={jest.fn()}
|
||||
search=""
|
||||
orderBy={null}
|
||||
sortOrder={SortOrder.ASC}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue