Parcourir la source

Fix topic form (#767)

Alexander Krivonosov il y a 3 ans
Parent
commit
eaf77c49a2

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

@@ -1,5 +1,5 @@
 import React from 'react';
-import { ClusterName, TopicName, TopicFormDataRaw } from 'redux/interfaces';
+import { ClusterName, TopicName, TopicFormData } from 'redux/interfaces';
 import { useForm, FormProvider } from 'react-hook-form';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { clusterTopicsPath } from 'lib/paths';
@@ -8,7 +8,7 @@ import TopicForm from 'components/Topics/shared/Form/TopicForm';
 interface Props {
   clusterName: ClusterName;
   isTopicCreated: boolean;
-  createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
+  createTopic: (clusterName: ClusterName, form: TopicFormData) => void;
   redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
   resetUploadedState: () => void;
 }
@@ -19,7 +19,7 @@ const New: React.FC<Props> = ({
   createTopic,
   redirectToTopicPath,
 }) => {
-  const methods = useForm<TopicFormDataRaw>();
+  const methods = useForm<TopicFormData>();
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
 
   React.useEffect(() => {
@@ -29,7 +29,7 @@ const New: React.FC<Props> = ({
     }
   }, [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods]);
 
-  const onSubmit = async (data: TopicFormDataRaw) => {
+  const onSubmit = async (data: TopicFormData) => {
     // TODO: need to fix loader. After success loading the first time, we won't wait for creation any more, because state is
     // loaded, and we will try to get entity immediately after pressing the button, and we will receive null
     // going to object page on the second creation. Setting of isSubmitting after createTopic is a workaround, need to tweak loader logic

+ 2 - 2
kafka-ui-react-app/src/components/Topics/New/NewContainer.ts

@@ -4,7 +4,7 @@ import {
   ClusterName,
   TopicName,
   Action,
-  TopicFormDataRaw,
+  TopicFormData,
 } from 'redux/interfaces';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { createTopic, createTopicAction } from 'redux/actions';
@@ -36,7 +36,7 @@ const mapDispatchToProps = (
   dispatch: ThunkDispatch<RootState, undefined, Action>,
   { history }: OwnProps
 ) => ({
-  createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => {
+  createTopic: (clusterName: ClusterName, form: TopicFormData) => {
     dispatch(createTopic(clusterName, form));
   },
   redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {

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

@@ -5,6 +5,7 @@ import {
   TopicName,
   TopicConfigByName,
   TopicWithDetailedInfo,
+  TopicFormData,
 } from 'redux/interfaces';
 import { TopicConfig } from 'generated-sources';
 import { useForm, FormProvider } from 'react-hook-form';
@@ -63,6 +64,9 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
     name,
     partitions: topic.partitionCount || DEFAULTS.partitions,
     replicationFactor,
+    customParams: topic.config
+      ?.filter((el) => el.value !== el.defaultValue)
+      .map((el) => ({ name: el.name, value: el.value })),
     ...configs,
   };
 };
@@ -80,7 +84,7 @@ const Edit: React.FC<Props> = ({
 }) => {
   const defaultValues = topicParams(topic);
 
-  const methods = useForm<TopicFormDataRaw>({ defaultValues });
+  const methods = useForm<TopicFormData>({ defaultValues });
 
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
   const history = useHistory();

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

@@ -1,51 +1,108 @@
 import React from 'react';
-import CustomParamSelect from 'components/Topics/shared/Form/CustomParams/CustomParamSelect';
-import CustomParamValue from 'components/Topics/shared/Form/CustomParams/CustomParamValue';
-import CustomParamAction from 'components/Topics/shared/Form/CustomParams/CustomParamAction';
-import { TopicConfig } from 'generated-sources';
+import { ErrorMessage } from '@hookform/error-message';
+import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
+import { FieldArrayWithId, useFormContext } from 'react-hook-form';
+import { remove as _remove } from 'lodash';
+import { TopicFormData } from 'redux/interfaces';
+
+import CustomParamButton from './CustomParamButton';
 
 interface Props {
   isDisabled: boolean;
-  index: string;
-  name: TopicConfig['name'];
+  index: number;
   existingFields: string[];
-  defaultValue: TopicConfig['defaultValue'];
-  onNameChange: (inputName: string, name: string) => void;
-  onRemove: (index: string) => void;
+  field: FieldArrayWithId<TopicFormData, 'customParams', 'id'>;
+  remove: (index?: number | number[] | undefined) => void;
+  setExistingFields: React.Dispatch<React.SetStateAction<string[]>>;
 }
 
 const CustomParamField: React.FC<Props> = ({
+  field,
   isDisabled,
   index,
-  name,
+  remove,
   existingFields,
-  defaultValue,
-  onNameChange,
-  onRemove,
+  setExistingFields,
 }) => {
+  const {
+    register,
+    formState: { errors },
+    setValue,
+    watch,
+  } = useFormContext<TopicFormData>();
+  const nameValue = watch(`customParams.${index}.name`);
+  let prevName = '';
+
+  React.useEffect(() => {
+    prevName = nameValue;
+  }, []);
+
+  React.useEffect(() => {
+    if (nameValue !== prevName) {
+      let newExistingFields = [...existingFields];
+      if (prevName) {
+        newExistingFields = _remove(newExistingFields, (el) => el === prevName);
+      }
+      prevName = nameValue;
+      newExistingFields.push(nameValue);
+      setExistingFields(newExistingFields);
+      setValue(`customParams.${index}.value`, TOPIC_CUSTOM_PARAMS[nameValue]);
+    }
+  }, [nameValue]);
+
   return (
     <div className="columns is-centered">
       <div className="column">
-        <CustomParamSelect
-          index={index}
-          isDisabled={isDisabled}
-          name={name}
-          existingFields={existingFields}
-          onNameChange={onNameChange}
-        />
+        <label className="label">Custom Parameter</label>
+        <div className="select is-block">
+          <select
+            {...register(`customParams.${index}.name` as const, {
+              required: 'Custom Parameter is required.',
+            })}
+            disabled={isDisabled}
+            defaultValue={field.name}
+          >
+            <option value="">Select</option>
+            {Object.keys(TOPIC_CUSTOM_PARAMS).map((opt) => (
+              <option
+                key={opt}
+                value={opt}
+                disabled={existingFields.includes(opt)}
+              >
+                {opt}
+              </option>
+            ))}
+          </select>
+          <p className="help is-danger">
+            <ErrorMessage errors={errors} name={`customParams.${index}.name`} />
+          </p>
+        </div>
       </div>
 
       <div className="column">
-        <CustomParamValue
-          index={index}
-          isDisabled={isDisabled}
-          name={name}
-          defaultValue={defaultValue}
+        <label className="label">Value</label>
+        <input
+          className="input"
+          placeholder="Value"
+          {...register(`customParams.${index}.value` as const, {
+            required: 'Value is required.',
+          })}
+          defaultValue={field.value}
+          autoComplete="off"
+          disabled={isDisabled}
         />
+        <p className="help is-danger">
+          <ErrorMessage errors={errors} name={`customParams.${index}.value`} />
+        </p>
       </div>
 
       <div className="column is-narrow">
-        <CustomParamAction index={index} onRemove={onRemove} />
+        <label className="label">&nbsp;</label>
+        <CustomParamButton
+          className="is-danger"
+          type="fa-minus"
+          onClick={() => remove(index)}
+        />
       </div>
     </div>
   );

+ 0 - 85
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamSelect.tsx

@@ -1,85 +0,0 @@
-import React from 'react';
-import { useFormContext } from 'react-hook-form';
-import { TopicConfigValue } from 'redux/interfaces';
-import { ErrorMessage } from '@hookform/error-message';
-import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
-
-import { INDEX_PREFIX } from './CustomParams';
-
-export interface CustomParamSelectProps {
-  isDisabled: boolean;
-  index: string;
-  name: string;
-  existingFields: string[];
-  onNameChange: (inputName: string, name: string) => void;
-}
-
-const CustomParamSelect: React.FC<CustomParamSelectProps> = ({
-  isDisabled,
-  index,
-  name,
-  existingFields,
-  onNameChange,
-}) => {
-  const {
-    register,
-    getValues,
-    trigger,
-    formState: { errors },
-  } = useFormContext();
-  const optInputName = `${index}[name]`;
-
-  const selectedMustBeUniq = (selected: string) => {
-    const values = getValues();
-    const customParamsValues: TopicConfigValue = values.customParams;
-
-    const valid = !Object.entries(customParamsValues).some(
-      ([key, customParam]) => {
-        return (
-          `${INDEX_PREFIX}.${key}` !== index && selected === customParam.name
-        );
-      }
-    );
-
-    return valid || 'Custom Parameter must be unique';
-  };
-
-  const onChange =
-    (inputName: string) => (event: React.ChangeEvent<HTMLSelectElement>) => {
-      trigger(inputName);
-      onNameChange(index, event.target.value);
-    };
-
-  return (
-    <>
-      <label className="label">Custom Parameter</label>
-      <div className="select is-block">
-        <select
-          {...register(optInputName, {
-            required: 'Custom Parameter is required.',
-            validate: { unique: (selected) => selectedMustBeUniq(selected) },
-          })}
-          onChange={onChange(optInputName)}
-          disabled={isDisabled}
-          defaultValue={name}
-        >
-          <option value="">Select</option>
-          {Object.keys(TOPIC_CUSTOM_PARAMS).map((opt) => (
-            <option
-              key={opt}
-              value={opt}
-              disabled={existingFields.includes(opt)}
-            >
-              {opt}
-            </option>
-          ))}
-        </select>
-        <p className="help is-danger">
-          <ErrorMessage errors={errors} name={optInputName} />
-        </p>
-      </div>
-    </>
-  );
-};
-
-export default React.memo(CustomParamSelect);

+ 0 - 56
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamValue.tsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import { useFormContext } from 'react-hook-form';
-import { TopicConfig } from 'generated-sources';
-import { ErrorMessage } from '@hookform/error-message';
-import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
-
-interface Props {
-  isDisabled: boolean;
-  index: string;
-  name: TopicConfig['name'];
-  defaultValue: TopicConfig['defaultValue'];
-}
-
-const CustomParamValue: React.FC<Props> = ({
-  isDisabled,
-  index,
-  name,
-  defaultValue,
-}) => {
-  const {
-    register,
-    watch,
-    setValue,
-    formState: { errors },
-  } = useFormContext();
-  const selectInputName = `${index}[name]`;
-  const valInputName = `${index}[value]`;
-  const selectedParamName = watch(selectInputName, name);
-
-  React.useEffect(() => {
-    if (selectedParamName && !defaultValue) {
-      setValue(valInputName, TOPIC_CUSTOM_PARAMS[selectedParamName]);
-    }
-  }, [selectedParamName, setValue, valInputName]);
-
-  return (
-    <>
-      <label className="label">Value</label>
-      <input
-        className="input"
-        placeholder="Value"
-        {...register(valInputName, {
-          required: 'Value is required.',
-        })}
-        defaultValue={defaultValue}
-        autoComplete="off"
-        disabled={isDisabled}
-      />
-      <p className="help is-danger">
-        <ErrorMessage errors={errors} name={name} />
-      </p>
-    </>
-  );
-};
-
-export default React.memo(CustomParamValue);

+ 16 - 69
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

@@ -1,11 +1,6 @@
 import React from 'react';
-import { omit, reject, reduce, remove } from 'lodash';
-import { v4 } from 'uuid';
-import {
-  TopicFormCustomParams,
-  TopicConfigByName,
-  TopicConfigParams,
-} from 'redux/interfaces';
+import { TopicConfigByName, TopicFormData } from 'redux/interfaces';
+import { useFieldArray, useFormContext } from 'react-hook-form';
 
 import CustomParamButton from './CustomParamButton';
 import CustomParamField from './CustomParamField';
@@ -17,59 +12,13 @@ interface Props {
   config?: TopicConfigByName;
 }
 
-const existingFields: string[] = [];
-
-const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
-  const byIndex = config
-    ? reduce(
-        config.byName,
-        (result: TopicConfigParams, param, paramName) => ({
-          ...result,
-          [`${INDEX_PREFIX}.${new Date().getTime()}ts`]: {
-            name: paramName,
-            value: param.value,
-            id: v4(),
-          },
-        }),
-        {}
-      )
-    : {};
-
-  const [formCustomParams, setFormCustomParams] =
-    React.useState<TopicFormCustomParams>({
-      byIndex,
-      allIndexes: Object.keys(byIndex),
-    });
-
-  const onAdd = (event: React.MouseEvent<HTMLButtonElement>) => {
-    event.preventDefault();
-
-    const newIndex = `${INDEX_PREFIX}.${new Date().getTime()}ts`;
-
-    setFormCustomParams({
-      ...formCustomParams,
-      byIndex: {
-        ...formCustomParams.byIndex,
-        [newIndex]: { name: '', value: '' },
-      },
-      allIndexes: [newIndex, ...formCustomParams.allIndexes],
-    });
-  };
-
-  const onRemove = (index: string) => {
-    const fieldName = formCustomParams.byIndex[index].name;
-    remove(existingFields, (el) => el === fieldName);
-    setFormCustomParams({
-      ...formCustomParams,
-      byIndex: omit(formCustomParams.byIndex, index),
-      allIndexes: reject(formCustomParams.allIndexes, (i) => i === index),
-    });
-  };
-
-  const onFieldNameChange = (index: string, name: string) => {
-    formCustomParams.byIndex[index].name = name;
-    existingFields.push(name);
-  };
+const CustomParams: React.FC<Props> = ({ isSubmitting }) => {
+  const { control } = useFormContext<TopicFormData>();
+  const { fields, append, remove } = useFieldArray({
+    control,
+    name: INDEX_PREFIX,
+  });
+  const [existingFields, setExistingFields] = React.useState<string[]>([]);
 
   return (
     <>
@@ -78,22 +27,20 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
           <CustomParamButton
             className="is-success"
             type="fa-plus"
-            onClick={onAdd}
+            onClick={() => append({ name: '', value: '' })}
             btnText="Add Custom Parameter"
           />
         </div>
       </div>
-
-      {formCustomParams.allIndexes.map((index) => (
+      {fields.map((field, idx) => (
         <CustomParamField
-          key={formCustomParams.byIndex[index].name}
-          index={index}
+          key={field.id}
+          field={field}
+          remove={remove}
+          index={idx}
           isDisabled={isSubmitting}
-          name={formCustomParams.byIndex[index].name}
-          defaultValue={formCustomParams.byIndex[index].value}
           existingFields={existingFields}
-          onNameChange={onFieldNameChange}
-          onRemove={onRemove}
+          setExistingFields={setExistingFields}
         />
       ))}
     </>

+ 0 - 49
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParamSelect.spec.tsx

@@ -1,49 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { useForm, FormProvider } from 'react-hook-form';
-import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
-import CustomParamSelect, {
-  CustomParamSelectProps,
-} from 'components/Topics/shared/Form/CustomParams/CustomParamSelect';
-
-const existingFields = [
-  'leader.replication.throttled.replicas',
-  'segment.index.bytes',
-  'message.timestamp.difference.max.ms',
-];
-
-const Wrapper: React.FC<Partial<CustomParamSelectProps>> = (props = {}) => {
-  const methods = useForm();
-  return (
-    <FormProvider {...methods}>
-      <CustomParamSelect
-        index="1"
-        name="my_custom_param"
-        existingFields={[]}
-        isDisabled
-        onNameChange={jest.fn()}
-        {...props}
-      />
-    </FormProvider>
-  );
-};
-
-describe('CustomParamSelect', () => {
-  it('renders correct options', () => {
-    const fieldsCount = Object.keys(TOPIC_CUSTOM_PARAMS).length;
-    const wrapper = mount(<Wrapper existingFields={existingFields} />);
-    const options = wrapper.find('option');
-    const disabledOptions = options.filterWhere((o) => !!o.prop('disabled'));
-
-    expect(options.length).toEqual(fieldsCount + 1);
-    expect(disabledOptions.length).toEqual(existingFields.length);
-  });
-
-  it('matches snapshot', () => {
-    expect(
-      mount(<Wrapper existingFields={existingFields} />).find(
-        'Memo(CustomParamSelect)'
-      )
-    ).toMatchSnapshot();
-  });
-});

+ 0 - 202
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/__snapshots__/CustomParamSelect.spec.tsx.snap

@@ -1,202 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CustomParamSelect matches snapshot 1`] = `
-<Memo(CustomParamSelect)
-  existingFields={
-    Array [
-      "leader.replication.throttled.replicas",
-      "segment.index.bytes",
-      "message.timestamp.difference.max.ms",
-    ]
-  }
-  index="1"
-  isDisabled={true}
-  name="my_custom_param"
-  onNameChange={[MockFunction]}
->
-  <label
-    className="label"
-  >
-    Custom Parameter
-  </label>
-  <div
-    className="select is-block"
-  >
-    <select
-      defaultValue="my_custom_param"
-      disabled={true}
-      name="1[name]"
-      onBlur={[Function]}
-      onChange={[Function]}
-    >
-      <option
-        value=""
-      >
-        Select
-      </option>
-      <option
-        disabled={false}
-        key="compression.type"
-        value="compression.type"
-      >
-        compression.type
-      </option>
-      <option
-        disabled={true}
-        key="leader.replication.throttled.replicas"
-        value="leader.replication.throttled.replicas"
-      >
-        leader.replication.throttled.replicas
-      </option>
-      <option
-        disabled={false}
-        key="message.downconversion.enable"
-        value="message.downconversion.enable"
-      >
-        message.downconversion.enable
-      </option>
-      <option
-        disabled={false}
-        key="segment.jitter.ms"
-        value="segment.jitter.ms"
-      >
-        segment.jitter.ms
-      </option>
-      <option
-        disabled={false}
-        key="flush.ms"
-        value="flush.ms"
-      >
-        flush.ms
-      </option>
-      <option
-        disabled={false}
-        key="follower.replication.throttled.replicas"
-        value="follower.replication.throttled.replicas"
-      >
-        follower.replication.throttled.replicas
-      </option>
-      <option
-        disabled={false}
-        key="segment.bytes"
-        value="segment.bytes"
-      >
-        segment.bytes
-      </option>
-      <option
-        disabled={false}
-        key="flush.messages"
-        value="flush.messages"
-      >
-        flush.messages
-      </option>
-      <option
-        disabled={false}
-        key="message.format.version"
-        value="message.format.version"
-      >
-        message.format.version
-      </option>
-      <option
-        disabled={false}
-        key="file.delete.delay.ms"
-        value="file.delete.delay.ms"
-      >
-        file.delete.delay.ms
-      </option>
-      <option
-        disabled={false}
-        key="max.compaction.lag.ms"
-        value="max.compaction.lag.ms"
-      >
-        max.compaction.lag.ms
-      </option>
-      <option
-        disabled={false}
-        key="min.compaction.lag.ms"
-        value="min.compaction.lag.ms"
-      >
-        min.compaction.lag.ms
-      </option>
-      <option
-        disabled={false}
-        key="message.timestamp.type"
-        value="message.timestamp.type"
-      >
-        message.timestamp.type
-      </option>
-      <option
-        disabled={false}
-        key="preallocate"
-        value="preallocate"
-      >
-        preallocate
-      </option>
-      <option
-        disabled={false}
-        key="min.cleanable.dirty.ratio"
-        value="min.cleanable.dirty.ratio"
-      >
-        min.cleanable.dirty.ratio
-      </option>
-      <option
-        disabled={false}
-        key="index.interval.bytes"
-        value="index.interval.bytes"
-      >
-        index.interval.bytes
-      </option>
-      <option
-        disabled={false}
-        key="unclean.leader.election.enable"
-        value="unclean.leader.election.enable"
-      >
-        unclean.leader.election.enable
-      </option>
-      <option
-        disabled={false}
-        key="retention.bytes"
-        value="retention.bytes"
-      >
-        retention.bytes
-      </option>
-      <option
-        disabled={false}
-        key="delete.retention.ms"
-        value="delete.retention.ms"
-      >
-        delete.retention.ms
-      </option>
-      <option
-        disabled={false}
-        key="segment.ms"
-        value="segment.ms"
-      >
-        segment.ms
-      </option>
-      <option
-        disabled={true}
-        key="message.timestamp.difference.max.ms"
-        value="message.timestamp.difference.max.ms"
-      >
-        message.timestamp.difference.max.ms
-      </option>
-      <option
-        disabled={true}
-        key="segment.index.bytes"
-        value="segment.index.bytes"
-      >
-        segment.index.bytes
-      </option>
-    </select>
-    <p
-      className="help is-danger"
-    >
-      <Component
-        errors={Object {}}
-        name="1[name]"
-      />
-    </p>
-  </div>
-</Memo(CustomParamSelect)>
-`;

+ 7 - 6
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -19,6 +19,7 @@ import {
   TopicFormDataRaw,
   TopicsState,
   FailurePayload,
+  TopicFormData,
 } from 'redux/interfaces';
 import { BASE_PARAMS } from 'lib/constants';
 import * as actions from 'redux/actions/actions';
@@ -161,7 +162,7 @@ const topicReducer = (
   };
 };
 
-const formatTopicCreation = (form: TopicFormDataRaw): TopicCreation => {
+const formatTopicCreation = (form: TopicFormData): TopicCreation => {
   const {
     name,
     partitions,
@@ -180,10 +181,10 @@ const formatTopicCreation = (form: TopicFormDataRaw): TopicCreation => {
     replicationFactor,
     configs: {
       'cleanup.policy': cleanupPolicy,
-      'retention.ms': retentionMs,
-      'retention.bytes': retentionBytes,
-      'max.message.bytes': maxMessageBytes,
-      'min.insync.replicas': minInSyncReplicas,
+      'retention.ms': retentionMs.toString(),
+      'retention.bytes': retentionBytes.toString(),
+      'max.message.bytes': maxMessageBytes.toString(),
+      'min.insync.replicas': minInSyncReplicas.toString(),
       ...Object.values(customParams || {}).reduce(topicReducer, {}),
     },
   };
@@ -212,7 +213,7 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
 };
 
 export const createTopic =
-  (clusterName: ClusterName, form: TopicFormDataRaw): PromiseThunkResult =>
+  (clusterName: ClusterName, form: TopicFormData): PromiseThunkResult =>
   async (dispatch, getState) => {
     dispatch(actions.createTopicAction.request());
     try {

+ 15 - 0
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -70,6 +70,21 @@ export interface TopicFormDataRaw {
   customParams: TopicFormCustomParams;
 }
 
+export interface TopicFormData {
+  name: string;
+  partitions: number;
+  replicationFactor: number;
+  minInSyncReplicas: number;
+  cleanupPolicy: string;
+  retentionMs: number;
+  retentionBytes: number;
+  maxMessageBytes: number;
+  customParams: {
+    name: string;
+    value: string;
+  }[];
+}
+
 export interface TopicMessagesState {
   messages: TopicMessage[];
   phase?: string;