Browse Source

Сlone topic functionality (FE) (#1825)

* Added Copy button in list of topics

* Added copy topic functionality

* Whitespaces removed

* Removed extra string wrapper

* Copy component removed, routing fixed, tests fixed

* Ternary and returning null removed

* Dublicated code refactored

* Added tests for ternary header

* Added enum for the form fields

Co-authored-by: k.morozov <k.morozov@ffin.ru>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Kirill Morozov 3 years ago
parent
commit
deddf09ed4

+ 30 - 1
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -6,7 +6,7 @@ import {
   TopicName,
 } from 'redux/interfaces';
 import { useParams } from 'react-router-dom';
-import { clusterTopicNewPath } from 'lib/paths';
+import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
 import usePagination from 'lib/hooks/usePagination';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -125,6 +125,21 @@ const List: React.FC<TopicsListProps> = ({
     }
   );
 
+  const getSelectedTopic = (): string => {
+    const name = Array.from(tableState.selectedIds)[0];
+    const selectedTopic =
+      tableState.data.find(
+        (topic: TopicWithDetailedInfo) => topic.name === name
+      ) || {};
+
+    return Object.keys(selectedTopic)
+      .map((x: string) => {
+        const value = selectedTopic[x as keyof typeof selectedTopic];
+        return value && x !== 'partitions' ? `${x}=${value}` : null;
+      })
+      .join('&');
+  };
+
   const handleSwitch = React.useCallback(() => {
     setShowInternal(!showInternal);
     history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
@@ -295,6 +310,20 @@ const List: React.FC<TopicsListProps> = ({
                 >
                   Delete selected topics
                 </Button>
+                {tableState.selectedCount === 1 && (
+                  <Button
+                    buttonSize="M"
+                    buttonType="secondary"
+                    isLink
+                    to={{
+                      pathname: clusterTopicCopyPath(clusterName),
+                      search: `?${getSelectedTopic()}`,
+                    }}
+                  >
+                    Copy selected topic
+                  </Button>
+                )}
+
                 <Button
                   buttonSize="M"
                   buttonType="secondary"

+ 24 - 2
kafka-ui-react-app/src/components/Topics/New/New.tsx

@@ -10,7 +10,7 @@ import {
 } from 'redux/actions';
 import { useDispatch } from 'react-redux';
 import { getResponse } from 'lib/errorHandling';
-import { useHistory, useParams } from 'react-router';
+import { useHistory, useLocation, useParams } from 'react-router';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { topicFormValidationSchema } from 'lib/yupExtended';
 import PageHeading from 'components/common/PageHeading/PageHeading';
@@ -19,6 +19,14 @@ interface RouterParams {
   clusterName: ClusterName;
 }
 
+enum Filters {
+  NAME = 'name',
+  PARTITION_COUNT = 'partitionCount',
+  REPLICATION_FACTOR = 'replicationFactor',
+  INSYNC_REPLICAS = 'inSyncReplicas',
+  CLEANUP_POLICY = 'Delete',
+}
+
 const New: React.FC = () => {
   const methods = useForm<TopicFormData>({
     mode: 'all',
@@ -29,6 +37,15 @@ const New: React.FC = () => {
   const history = useHistory();
   const dispatch = useDispatch();
 
+  const { search } = useLocation();
+  const params = new URLSearchParams(search);
+
+  const name = params.get(Filters.NAME) || '';
+  const partitionCount = params.get(Filters.PARTITION_COUNT) || 1;
+  const replicationFactor = params.get(Filters.REPLICATION_FACTOR) || 1;
+  const inSyncReplicas = params.get(Filters.INSYNC_REPLICAS) || 1;
+  const cleanUpPolicy = params.get(Filters.CLEANUP_POLICY) || 'Delete';
+
   const onSubmit = async (data: TopicFormData) => {
     try {
       await topicsApiClient.createTopic({
@@ -50,9 +67,14 @@ const New: React.FC = () => {
 
   return (
     <>
-      <PageHeading text="Create new Topic" />
+      <PageHeading text={search ? 'Copy Topic' : 'Create new Topic'} />
       <FormProvider {...methods}>
         <TopicForm
+          topicName={name}
+          cleanUpPolicy={cleanUpPolicy}
+          partitionCount={Number(partitionCount)}
+          replicationFactor={Number(replicationFactor)}
+          inSyncReplicas={Number(inSyncReplicas)}
           isSubmitting={methods.formState.isSubmitting}
           onSubmit={methods.handleSubmit(onSubmit)}
         />

+ 36 - 1
kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

@@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
 import { screen, waitFor } from '@testing-library/react';
 import { createMemoryHistory } from 'history';
 import fetchMock from 'fetch-mock-jest';
-import { clusterTopicNewPath, clusterTopicPath } from 'lib/paths';
+import {
+  clusterTopicCopyPath,
+  clusterTopicNewPath,
+  clusterTopicPath,
+} from 'lib/paths';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
@@ -31,6 +35,11 @@ const renderComponent = (history = historyMock, store = storeMock) =>
           <New />
         </Provider>
       </Route>
+      <Route path={clusterTopicCopyPath(':clusterName')}>
+        <Provider store={store}>
+          <New />
+        </Provider>
+      </Route>
       <Route path={clusterTopicPath(':clusterName', ':topicName')}>
         New topic path
       </Route>
@@ -42,6 +51,32 @@ describe('New', () => {
     fetchMock.reset();
   });
 
+  it('checks header for create new', async () => {
+    const mockedHistory = createMemoryHistory({
+      initialEntries: [clusterTopicNewPath(clusterName)],
+    });
+    renderComponent(mockedHistory);
+    expect(
+      screen.getByRole('heading', { name: 'Create new Topic' })
+    ).toHaveTextContent('Create new Topic');
+  });
+
+  it('checks header for copy', async () => {
+    const mockedHistory = createMemoryHistory({
+      initialEntries: [
+        {
+          pathname: clusterTopicCopyPath(clusterName),
+          search: `?name=test`,
+        },
+      ],
+    });
+
+    renderComponent(mockedHistory);
+    expect(
+      screen.getByRole('heading', { name: 'Copy Topic' })
+    ).toHaveTextContent('Copy Topic');
+  });
+
   it('validates form', async () => {
     const mockedHistory = createMemoryHistory({
       initialEntries: [clusterTopicNewPath(clusterName)],

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

@@ -1,6 +1,7 @@
 import React from 'react';
 import { Switch } from 'react-router-dom';
 import {
+  clusterTopicCopyPath,
   clusterTopicNewPath,
   clusterTopicPath,
   clusterTopicsPath,
@@ -23,6 +24,11 @@ const Topics: React.FC = () => (
       path={clusterTopicNewPath(':clusterName')}
       component={New}
     />
+    <BreadcrumbRoute
+      exact
+      path={clusterTopicCopyPath(':clusterName')}
+      component={New}
+    />
     <BreadcrumbRoute
       path={clusterTopicPath(':clusterName', ':topicName')}
       component={TopicContainer}

+ 16 - 4
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -16,6 +16,10 @@ import * as S from './TopicForm.styled';
 
 export interface Props {
   topicName?: TopicName;
+  partitionCount?: number;
+  replicationFactor?: number;
+  inSyncReplicas?: number;
+  cleanUpPolicy?: string;
   isEditing?: boolean;
   isSubmitting: boolean;
   onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
@@ -40,11 +44,19 @@ const TopicForm: React.FC<Props> = ({
   isEditing,
   isSubmitting,
   onSubmit,
+  partitionCount,
+  replicationFactor,
+  inSyncReplicas,
+  cleanUpPolicy,
 }) => {
   const {
     control,
     formState: { errors },
   } = useFormContext();
+  const getCleanUpPolicy =
+    CleanupPolicyOptions.find((option: SelectOption) => {
+      return option.value === cleanUpPolicy?.toLowerCase();
+    })?.value || CleanupPolicyOptions[0].value;
   return (
     <StyledForm onSubmit={onSubmit}>
       <fieldset disabled={isSubmitting}>
@@ -75,7 +87,7 @@ const TopicForm: React.FC<Props> = ({
                   type="number"
                   placeholder="Number of partitions"
                   min="1"
-                  defaultValue="1"
+                  defaultValue={partitionCount}
                   name="partitions"
                 />
                 <FormError>
@@ -91,7 +103,7 @@ const TopicForm: React.FC<Props> = ({
                   type="number"
                   placeholder="Replication Factor"
                   min="1"
-                  defaultValue="1"
+                  defaultValue={replicationFactor}
                   name="replicationFactor"
                 />
                 <FormError>
@@ -112,7 +124,7 @@ const TopicForm: React.FC<Props> = ({
               type="number"
               placeholder="Min In Sync Replicas"
               min="1"
-              defaultValue="1"
+              defaultValue={inSyncReplicas}
               name="minInsyncReplicas"
             />
             <FormError>
@@ -135,7 +147,7 @@ const TopicForm: React.FC<Props> = ({
                   id="topicFormCleanupPolicy"
                   aria-labelledby="topicFormCleanupPolicyLabel"
                   name={name}
-                  value={CleanupPolicyOptions[0].value}
+                  value={getCleanUpPolicy}
                   onChange={onChange}
                   minWidth="250px"
                   options={CleanupPolicyOptions}

+ 2 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -53,6 +53,8 @@ export const clusterTopicsPath = (clusterName: ClusterName) =>
   `${clusterPath(clusterName)}/topics`;
 export const clusterTopicNewPath = (clusterName: ClusterName) =>
   `${clusterPath(clusterName)}/topics/create-new`;
+export const clusterTopicCopyPath = (clusterName: ClusterName) =>
+  `${clusterPath(clusterName)}/topics/copy`;
 export const clusterTopicPath = (
   clusterName: ClusterName,
   topicName: TopicName