浏览代码

[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>
Zorii4 3 年之前
父节点
当前提交
634406ac91

+ 26 - 0
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -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>
         </>
       );
     }

+ 2 - 0
kafka-ui-react-app/src/components/Topics/List/ListContainer.ts

@@ -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,

+ 25 - 0
kafka-ui-react-app/src/components/Topics/List/ListItem.tsx

@@ -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>
   );

+ 1 - 0
kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx

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

+ 2 - 1
kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx

@@ -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}

+ 24 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -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

+ 2 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts

@@ -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,
 };

+ 39 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -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();
+  });
 });

+ 32 - 0
kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts

@@ -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(

+ 8 - 0
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -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(

+ 15 - 0
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -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 - 0
kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts

@@ -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', () => {

+ 8 - 0
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -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,