Forráskód Böngészése

UI support for MSK (#2828)

* UI support for MSK

* fixed test changes

* fixed pull request comments

* Use the opposite operator (!==) instead.

* fixed update topic

* Minor code modifications in the Edit Submission logic

* minor typescript code change, in the TopicFormData

* minor CustomParamField short circuit code efficiency improveral

* Topic Form Edit , sanitization of Default non-dirty fields, while sending the dynamic ones

* fix checkTopicCreatePossibility()

* fix checkTopicCreatePossibility()

Co-authored-by: Mgrdich <46796009+Mgrdich@users.noreply.github.com>
Co-authored-by: Mgrdich <mgotm13@gmail.com>
Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>
Hrant Abrahamyan 2 éve
szülő
commit
b0c897b5c8

+ 2 - 2
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java

@@ -9,8 +9,8 @@ import lombok.experimental.Accessors;
 @Data
 @Accessors(chain = true)
 public class Topic {
-    private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent,
-            partitions, customParameterValue;
+    private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue;
+    private int numberOfPartitions;
     private CustomParameterType customParameterType;
     private CleanupPolicyValue cleanupPolicyValue;
     private MaxSizeOnDisk maxSizeOnDisk;

+ 7 - 3
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicCreateEditForm.java

@@ -48,7 +48,10 @@ public class TopicCreateEditForm extends BasePage {
 
   @Step
   public TopicCreateEditForm setTopicName(String topicName) {
-    nameField.setValue(topicName);
+    nameField.shouldBe(Condition.enabled).clear();
+    if (topicName != null) {
+      nameField.sendKeys(topicName);
+    }
     return this;
   }
 
@@ -108,8 +111,9 @@ public class TopicCreateEditForm extends BasePage {
   }
 
   @Step
-  public TopicCreateEditForm setPartitions(String partitions) {
-    partitionsField.setValue(partitions);
+  public TopicCreateEditForm setNumberOfPartitions(int partitions) {
+    partitionsField.shouldBe(Condition.enabled).clear();
+    partitionsField.sendKeys(String.valueOf(partitions));
     return this;
   }
 

+ 2 - 2
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java

@@ -85,8 +85,8 @@ public class TopicDetails extends BasePage {
   }
 
   @Step
-  public String getPartitions() {
-    return partitionsField.getText();
+  public int getPartitions() {
+    return Integer.parseInt(partitionsField.getText().trim());
   }
 
   @Step

+ 14 - 6
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java

@@ -8,6 +8,7 @@ import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_20_GB;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang3.RandomUtils.nextInt;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import com.codeborne.selenide.Condition;
@@ -40,7 +41,7 @@ public class TopicsTests extends BaseTest {
   private static final String SUITE_TITLE = "Topics";
   private static final Topic TOPIC_TO_CREATE = new Topic()
       .setName("new-topic-" + randomAlphabetic(5))
-      .setPartitions("1")
+      .setNumberOfPartitions(1)
       .setCustomParameterType(COMPRESSION_TYPE)
       .setCustomParameterValue("producer")
       .setCleanupPolicyValue(DELETE);
@@ -77,7 +78,7 @@ public class TopicsTests extends BaseTest {
     topicCreateEditForm
         .waitUntilScreenReady()
         .setTopicName(TOPIC_TO_CREATE.getName())
-        .setPartitions(TOPIC_TO_CREATE.getPartitions())
+        .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions())
         .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue())
         .clickCreateTopicBtn();
     topicDetails
@@ -92,7 +93,8 @@ public class TopicsTests extends BaseTest {
         .isTrue();
     softly.assertThat(topicDetails.getCleanUpPolicy()).as("getCleanUpPolicy()")
         .isEqualTo(TOPIC_TO_CREATE.getCleanupPolicyValue().toString());
-    softly.assertThat(topicDetails.getPartitions()).as("getPartitions()").isEqualTo(TOPIC_TO_CREATE.getPartitions());
+    softly.assertThat(topicDetails.getPartitions()).as("getPartitions()")
+        .isEqualTo(TOPIC_TO_CREATE.getNumberOfPartitions());
     softly.assertAll();
     naviSideBar
         .openSideMenu(TOPICS);
@@ -237,11 +239,17 @@ public class TopicsTests extends BaseTest {
         .waitUntilScreenReady()
         .clickAddTopicBtn();
     topicCreateEditForm
-        .waitUntilScreenReady()
-        .setTopicName("");
+        .waitUntilScreenReady();
+    assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
+    topicCreateEditForm
+        .setTopicName("testName");
+    assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
+    topicCreateEditForm
+        .setTopicName(null)
+        .setNumberOfPartitions(nextInt(1, 10));
     assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
     topicCreateEditForm
-        .setTopicName("testTopic1");
+        .setTopicName("testName");
     assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isTrue();
   }
 

+ 24 - 11
kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import New from 'components/Topics/New/New';
 import { Route, Routes } from 'react-router-dom';
-import { act, screen, waitFor } from '@testing-library/react';
+import { act, screen } from '@testing-library/react';
 import {
   clusterTopicCopyPath,
   clusterTopicNewPath,
@@ -13,18 +13,17 @@ import { useCreateTopic } from 'lib/hooks/api/topics';
 
 const clusterName = 'local';
 const topicName = 'test-topic';
+const minValue = '1';
 
 const mockNavigate = jest.fn();
 jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
   useNavigate: () => mockNavigate,
 }));
-
 jest.mock('lib/hooks/api/topics', () => ({
   useCreateTopic: jest.fn(),
 }));
-
-const renderComponent = (path: string) =>
+const renderComponent = (path: string) => {
   render(
     <Routes>
       <Route path={clusterTopicNewPath()} element={<New />} />
@@ -33,9 +32,9 @@ const renderComponent = (path: string) =>
     </Routes>,
     { initialEntries: [path] }
   );
+};
 
 const createTopicMock = jest.fn();
-
 describe('New', () => {
   beforeEach(() => {
     (useCreateTopic as jest.Mock).mockImplementation(() => ({
@@ -45,7 +44,6 @@ describe('New', () => {
   afterEach(() => {
     mockNavigate.mockClear();
   });
-
   it('checks header for create new', async () => {
     await act(() => {
       renderComponent(clusterTopicNewPath(clusterName));
@@ -57,14 +55,25 @@ describe('New', () => {
     renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`);
     expect(screen.getByRole('heading', { name: 'Copy' })).toBeInTheDocument();
   });
-
   it('validates form', async () => {
-    renderComponent(clusterTopicNewPath(clusterName));
+    await renderComponent(clusterTopicNewPath(clusterName));
     await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
     await userEvent.clear(screen.getByPlaceholderText('Topic Name'));
-    await waitFor(() => {
-      expect(screen.getByText('name is a required field')).toBeInTheDocument();
-    });
+    await userEvent.tab();
+    await expect(
+      screen.getByText('name is a required field')
+    ).toBeInTheDocument();
+
+    await userEvent.type(
+      screen.getByLabelText('Number of partitions *'),
+      minValue
+    );
+    await userEvent.clear(screen.getByLabelText('Number of partitions *'));
+    await userEvent.tab();
+    await expect(
+      screen.getByText('Number of partitions is required and must be a number')
+    ).toBeInTheDocument();
+
     expect(createTopicMock).not.toHaveBeenCalled();
     expect(mockNavigate).not.toHaveBeenCalled();
   });
@@ -83,6 +92,10 @@ describe('New', () => {
   it('submits valid form', async () => {
     renderComponent(clusterTopicNewPath(clusterName));
     await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
+    await userEvent.type(
+      screen.getByLabelText('Number of partitions *'),
+      minValue
+    );
     await userEvent.click(screen.getByText('Create topic'));
     expect(createTopicMock).toHaveBeenCalledTimes(1);
     expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);

+ 19 - 2
kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -19,6 +19,7 @@ import {
   useUpdateTopic,
 } from 'lib/hooks/api/topics';
 import DangerZone from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
+import { ConfigSource } from 'generated-sources';
 
 export const TOPIC_EDIT_FORM_DEFAULT_PROPS = {
   partitions: 1,
@@ -54,9 +55,24 @@ const Edit: React.FC = () => {
   topicConfig?.forEach((param) => {
     config.byName[param.name] = param;
   });
-
   const onSubmit = async (data: TopicFormDataRaw) => {
-    await updateTopic.mutateAsync(data);
+    const filteredDirtyDefaultEntries = Object.entries(data).filter(
+      ([key, val]) => {
+        const isDirty =
+          String(val) !==
+          String(defaultValues[key as keyof typeof defaultValues]);
+
+        const isDefaultConfig =
+          config.byName[key]?.source === ConfigSource.DEFAULT_CONFIG;
+
+        // if it is changed should be sent or if it was Dynamic
+        return isDirty || !isDefaultConfig;
+      }
+    );
+
+    const newData = Object.fromEntries(filteredDirtyDefaultEntries);
+
+    await updateTopic.mutateAsync(newData);
     navigate('../');
   };
 
@@ -64,6 +80,7 @@ const Edit: React.FC = () => {
     <>
       <FormProvider {...methods}>
         <TopicForm
+          config={config.byName}
           topicName={topicName}
           retentionBytes={defaultValues.retentionBytes}
           inSyncReplicas={Number(defaultValues.minInSyncReplicas)}

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx

@@ -70,7 +70,7 @@ describe('Edit Component', () => {
     renderComponent();
     const btn = screen.getAllByText(/Update topic/i)[0];
     const field = screen.getByRole('spinbutton', {
-      name: 'Min In Sync Replicas * Min In Sync Replicas *',
+      name: 'Min In Sync Replicas Min In Sync Replicas',
     });
     await userEvent.type(field, '1');
     await userEvent.click(btn);

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts

@@ -20,7 +20,7 @@ const topicParamsTransformer = (topic?: Topic, config?: TopicConfig[]) => {
 
   const customParams = config.reduce((acc, { name, value, defaultValue }) => {
     if (value === defaultValue) return acc;
-    if (!Object.keys(TOPIC_CUSTOM_PARAMS).includes(name)) return acc;
+    if (!TOPIC_CUSTOM_PARAMS[name]) return acc;
     return [...acc, { name, value }];
   }, [] as { name: string; value?: string }[]);
 

+ 8 - 2
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx

@@ -2,7 +2,7 @@ import React, { useRef } from 'react';
 import { ErrorMessage } from '@hookform/error-message';
 import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
 import { FieldArrayWithId, useFormContext, Controller } from 'react-hook-form';
-import { TopicFormData } from 'redux/interfaces';
+import { TopicConfigParams, TopicFormData } from 'redux/interfaces';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { FormError } from 'components/common/Input/Input.styled';
 import Select from 'components/common/Select/Select';
@@ -10,10 +10,12 @@ import Input from 'components/common/Input/Input';
 import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import CloseIcon from 'components/common/Icons/CloseIcon';
 import * as C from 'components/Topics/shared/Form/TopicForm.styled';
+import { ConfigSource } from 'generated-sources';
 
 import * as S from './CustomParams.styled';
 
 export interface Props {
+  config?: TopicConfigParams;
   isDisabled: boolean;
   index: number;
   existingFields: string[];
@@ -27,6 +29,7 @@ const CustomParamField: React.FC<Props> = ({
   isDisabled,
   index,
   remove,
+  config,
   existingFields,
   setExistingFields,
 }) => {
@@ -44,7 +47,10 @@ const CustomParamField: React.FC<Props> = ({
     .map((option) => ({
       value: option,
       label: option,
-      disabled: existingFields.includes(option),
+      disabled:
+        (config &&
+          config[option].source !== ConfigSource.DYNAMIC_TOPIC_CONFIG) ||
+        existingFields.includes(option),
     }));
 
   React.useEffect(() => {

+ 9 - 3
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { TopicFormData } from 'redux/interfaces';
+import { TopicConfigParams, TopicFormData } from 'redux/interfaces';
 import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
 import { Button } from 'components/common/Button/Button';
 import { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants';
@@ -9,10 +9,15 @@ import CustomParamField from './CustomParamField';
 import * as S from './CustomParams.styled';
 
 export interface CustomParamsProps {
+  config?: TopicConfigParams;
   isSubmitting: boolean;
+  isEditing?: boolean;
 }
 
-const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
+const CustomParams: React.FC<CustomParamsProps> = ({
+  isSubmitting,
+  config,
+}) => {
   const { control } = useFormContext<TopicFormData>();
   const { fields, append, remove } = useFieldArray({
     control,
@@ -41,9 +46,10 @@ const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
 
   return (
     <S.ParamsWrapper>
-      {controlledFields.map((field, idx) => (
+      {controlledFields?.map((field, idx) => (
         <CustomParamField
           key={field.id}
+          config={config}
           field={field}
           remove={removeField}
           index={idx}

+ 1 - 1
kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx

@@ -39,7 +39,7 @@ const TimeToRetain: React.FC<Props> = ({ isSubmitting }) => {
       <Input
         id="timeToRetain"
         type="number"
-        defaultValue={defaultValue}
+        placeholder=" Time to retain data (in ms)"
         name={name}
         hookFormOptions={{
           min: { value: -1, message: 'must be greater than or equal to -1' },

+ 48 - 46
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { useFormContext, Controller } from 'react-hook-form';
 import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
-import { ClusterName, TopicName } from 'redux/interfaces';
+import { ClusterName, TopicConfigParams, TopicName } from 'redux/interfaces';
 import { ErrorMessage } from '@hookform/error-message';
 import Select, { SelectOption } from 'components/common/Select/Select';
 import Input from 'components/common/Input/Input';
@@ -18,6 +18,7 @@ import TimeToRetain from './TimeToRetain';
 import * as S from './TopicForm.styled';
 
 export interface Props {
+  config?: TopicConfigParams;
   topicName?: TopicName;
   partitionCount?: number;
   replicationFactor?: number;
@@ -44,14 +45,12 @@ const RetentionBytesOptions: Array<SelectOption> = [
 ];
 
 const TopicForm: React.FC<Props> = ({
+  config,
   retentionBytes,
   topicName,
   isEditing,
   isSubmitting,
   onSubmit,
-  partitionCount,
-  replicationFactor,
-  inSyncReplicas,
   cleanUpPolicy,
 }) => {
   const {
@@ -109,7 +108,6 @@ const TopicForm: React.FC<Props> = ({
                   type="number"
                   placeholder="Number of partitions"
                   min="1"
-                  defaultValue={partitionCount}
                   name="partitions"
                 />
                 <FormError>
@@ -117,20 +115,28 @@ const TopicForm: React.FC<Props> = ({
                 </FormError>
               </div>
               <div>
-                <InputLabel htmlFor="topicFormReplicationFactor">
-                  Replication Factor *
+                <InputLabel
+                  id="topicFormCleanupPolicyLabel"
+                  htmlFor="topicFormCleanupPolicy"
+                >
+                  Cleanup policy
                 </InputLabel>
-                <Input
-                  id="topicFormReplicationFactor"
-                  type="number"
-                  placeholder="Replication Factor"
-                  min="1"
-                  defaultValue={replicationFactor}
-                  name="replicationFactor"
+                <Controller
+                  defaultValue={CleanupPolicyOptions[0].value}
+                  control={control}
+                  name="cleanupPolicy"
+                  render={({ field: { name, onChange } }) => (
+                    <Select
+                      id="topicFormCleanupPolicy"
+                      aria-labelledby="topicFormCleanupPolicyLabel"
+                      name={name}
+                      value={getCleanUpPolicy}
+                      onChange={onChange}
+                      minWidth="250px"
+                      options={CleanupPolicyOptions}
+                    />
+                  )}
                 />
-                <FormError>
-                  <ErrorMessage errors={errors} name="replicationFactor" />
-                </FormError>
               </div>
             </S.Column>
           )}
@@ -139,44 +145,36 @@ const TopicForm: React.FC<Props> = ({
         <S.Column>
           <div>
             <InputLabel htmlFor="topicFormMinInSyncReplicas">
-              Min In Sync Replicas *
+              Min In Sync Replicas
             </InputLabel>
             <Input
               id="topicFormMinInSyncReplicas"
               type="number"
               placeholder="Min In Sync Replicas"
               min="1"
-              defaultValue={inSyncReplicas}
               name="minInSyncReplicas"
             />
             <FormError>
               <ErrorMessage errors={errors} name="minInSyncReplicas" />
             </FormError>
           </div>
-          <div>
-            <InputLabel
-              id="topicFormCleanupPolicyLabel"
-              htmlFor="topicFormCleanupPolicy"
-            >
-              Cleanup policy
-            </InputLabel>
-            <Controller
-              defaultValue={CleanupPolicyOptions[0].value}
-              control={control}
-              name="cleanupPolicy"
-              render={({ field: { name, onChange } }) => (
-                <Select
-                  id="topicFormCleanupPolicy"
-                  aria-labelledby="topicFormCleanupPolicyLabel"
-                  name={name}
-                  value={getCleanUpPolicy}
-                  onChange={onChange}
-                  minWidth="250px"
-                  options={CleanupPolicyOptions}
-                />
-              )}
-            />
-          </div>
+          {!isEditing && (
+            <div>
+              <InputLabel htmlFor="topicFormReplicationFactor">
+                Replication Factor
+              </InputLabel>
+              <Input
+                id="topicFormReplicationFactor"
+                type="number"
+                placeholder="Replication Factor"
+                min="1"
+                name="replicationFactor"
+              />
+              <FormError>
+                <ErrorMessage errors={errors} name="replicationFactor" />
+              </FormError>
+            </div>
+          )}
         </S.Column>
 
         <S.Column>
@@ -213,13 +211,13 @@ const TopicForm: React.FC<Props> = ({
 
           <div>
             <InputLabel htmlFor="topicFormMaxMessageBytes">
-              Maximum message size in bytes *
+              Maximum message size in bytes
             </InputLabel>
             <Input
               id="topicFormMaxMessageBytes"
               type="number"
+              placeholder="Maximum message size"
               min="1"
-              defaultValue="1000012"
               name="maxMessageBytes"
             />
             <FormError>
@@ -229,7 +227,11 @@ const TopicForm: React.FC<Props> = ({
         </S.Column>
 
         <S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>
-        <CustomParams isSubmitting={isSubmitting} />
+        <CustomParams
+          config={config}
+          isSubmitting={isSubmitting}
+          isEditing={isEditing}
+        />
         <S.ButtonWrapper>
           <Button
             type="button"

+ 3 - 3
kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx

@@ -36,9 +36,9 @@ describe('TopicForm', () => {
     expectByRoleAndNameToBeInDocument('textbox', 'Topic Name *');
 
     expectByRoleAndNameToBeInDocument('spinbutton', 'Number of partitions *');
-    expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor *');
+    expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor');
 
-    expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas *');
+    expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas');
     expectByRoleAndNameToBeInDocument('listbox', 'Cleanup policy');
 
     expectByRoleAndNameToBeInDocument(
@@ -53,7 +53,7 @@ describe('TopicForm', () => {
     expectByRoleAndNameToBeInDocument('listbox', 'Max size on disk in GB');
     expectByRoleAndNameToBeInDocument(
       'spinbutton',
-      'Maximum message size in bytes *'
+      'Maximum message size in bytes'
     );
 
     expectByRoleAndNameToBeInDocument('heading', 'Custom parameters');

+ 30 - 12
kafka-ui-react-app/src/lib/hooks/api/topics.ts

@@ -82,19 +82,33 @@ const formatTopicCreation = (form: TopicFormData): TopicCreation => {
     customParams,
   } = form;
 
-  return {
+  const configs = {
+    'cleanup.policy': cleanupPolicy,
+    'retention.ms': retentionMs.toString(),
+    'retention.bytes': retentionBytes.toString(),
+    'max.message.bytes': maxMessageBytes.toString(),
+    'min.insync.replicas': minInSyncReplicas.toString(),
+    ...Object.values(customParams || {}).reduce(topicReducer, {}),
+  };
+
+  const cleanConfigs = () => {
+    return Object.fromEntries(
+      Object.entries(configs).filter(([, val]) => val !== '')
+    );
+  };
+
+  const topicsvalue = {
     name,
     partitions,
-    replicationFactor,
-    configs: {
-      'cleanup.policy': cleanupPolicy,
-      'retention.ms': retentionMs.toString(),
-      'retention.bytes': retentionBytes.toString(),
-      'max.message.bytes': maxMessageBytes.toString(),
-      'min.insync.replicas': minInSyncReplicas.toString(),
-      ...Object.values(customParams || {}).reduce(topicReducer, {}),
-    },
+    configs: cleanConfigs(),
   };
+
+  return replicationFactor.toString() !== ''
+    ? {
+        ...topicsvalue,
+        replicationFactor,
+      }
+    : topicsvalue;
 };
 
 export function useCreateTopic(clusterName: ClusterName) {
@@ -141,8 +155,12 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
 export function useUpdateTopic(props: GetTopicDetailsRequest) {
   const client = useQueryClient();
   return useMutation(
-    (data: TopicFormDataRaw) =>
-      api.updateTopic({ ...props, topicUpdate: formatTopicUpdate(data) }),
+    (data: TopicFormDataRaw) => {
+      return api.updateTopic({
+        ...props,
+        topicUpdate: formatTopicUpdate(data),
+      });
+    },
     {
       onSuccess: () => {
         showSuccessAlert({

+ 4 - 21
kafka-ui-react-app/src/lib/yupExtended.ts

@@ -59,29 +59,12 @@ export const topicFormValidationSchema = yup.object().shape({
     .max(2147483647)
     .required()
     .typeError('Number of partitions is required and must be a number'),
-  replicationFactor: yup
-    .number()
-    .min(1)
-    .max(2147483647)
-    .required()
-    .typeError('Replication factor is required and must be a number'),
-  minInSyncReplicas: yup
-    .number()
-    .min(1)
-    .max(2147483647)
-    .required()
-    .typeError('Min in sync replicas is required and must be a number'),
+  replicationFactor: yup.string(),
+  minInSyncReplicas: yup.string(),
   cleanupPolicy: yup.string().required(),
-  retentionMs: yup
-    .number()
-    .min(-1, 'Must be greater than or equal to -1')
-    .typeError('Time to retain data is required and must be a number'),
+  retentionMs: yup.string(),
   retentionBytes: yup.number(),
-  maxMessageBytes: yup
-    .number()
-    .min(1)
-    .required()
-    .typeError('Maximum message size is required and must be a number'),
+  maxMessageBytes: yup.string(),
   customParams: yup.array().of(
     yup.object().shape({
       name: yup.string().required('Custom parameter is required'),

+ 4 - 2
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -8,7 +8,7 @@ import {
 
 export type TopicName = Topic['name'];
 
-interface TopicConfigParams {
+export interface TopicConfigParams {
   [paramName: string]: TopicConfig;
 }
 
@@ -23,7 +23,7 @@ interface TopicFormCustomParams {
 
 export type TopicFormFormattedParams = TopicCreation['configs'];
 
-export interface TopicFormDataRaw {
+interface TopicFormDataModified {
   name: string;
   partitions: number;
   replicationFactor: number;
@@ -35,6 +35,8 @@ export interface TopicFormDataRaw {
   customParams: TopicFormCustomParams;
 }
 
+export type TopicFormDataRaw = Partial<TopicFormDataModified>;
+
 export interface TopicFormData {
   name: string;
   partitions: number;