Repository: provectus/kafka-ui
Issue: customizable: Time to retain data
Labels: scope/frontend

Field Time to retain data in topic creation should be customizable. Now it's only allow to choose from enum

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json
index 4a2ad5ff718..1ba845e9bae 100644
--- a/kafka-ui-react-app/package-lock.json
+++ b/kafka-ui-react-app/package-lock.json
@@ -10478,6 +10478,11 @@
         "json-parse-better-errors": "^1.0.1"
       }
     },
+    "parse-ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
+      "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
+    },
     "parse5": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
@@ -11686,6 +11691,14 @@
         "react-is": "^16.8.4"
       }
     },
+    "pretty-ms": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-6.0.1.tgz",
+      "integrity": "sha512-ke4njoVmlotekHlHyCZ3wI/c5AMT8peuHs8rKJqekj/oR5G8lND2dVpicFlUz5cbZgE290vvkMuDwfj/OcW1kw==",
+      "requires": {
+        "parse-ms": "^2.1.0"
+      }
+    },
     "private": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json
index 9f52b99ed9d..cee64d5651e 100644
--- a/kafka-ui-react-app/package.json
+++ b/kafka-ui-react-app/package.json
@@ -22,6 +22,7 @@
     "json-server": "^0.15.1",
     "lodash": "^4.17.15",
     "node-sass": "^4.13.1",
+    "pretty-ms": "^6.0.1",
     "react": "^16.12.0",
     "react-dom": "^16.12.0",
     "react-hook-form": "^4.5.5",
diff --git a/kafka-ui-react-app/src/components/Topics/New/New.tsx b/kafka-ui-react-app/src/components/Topics/New/New.tsx
index c5dfe688df9..51b53f6078b 100644
--- a/kafka-ui-react-app/src/components/Topics/New/New.tsx
+++ b/kafka-ui-react-app/src/components/Topics/New/New.tsx
@@ -2,12 +2,13 @@ import React from 'react';
 import { ClusterName, CleanupPolicy, TopicFormData, TopicName } from 'redux/interfaces';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { clusterTopicsPath } from 'lib/paths';
-import { useForm, ErrorMessage } from 'react-hook-form';
+import { useForm, FormContext, ErrorMessage } from 'react-hook-form';
+
 import {
   TOPIC_NAME_VALIDATION_PATTERN,
-  MILLISECONDS_IN_DAY,
   BYTES_IN_GB,
 } from 'lib/constants';
+import TimeToRetain from './TimeToRetain';
 
 interface Props {
   clusterName: ClusterName;
@@ -24,17 +25,17 @@ const New: React.FC<Props> = ({
                                 redirectToTopicPath,
                                 resetUploadedState
                               }) => {
-  const {register, handleSubmit, errors, getValues} = useForm<TopicFormData>();
+  const methods = useForm<TopicFormData>();
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
 
   React.useEffect(
     () => {
       if (isSubmitting && isTopicCreated) {
-        const {name} = getValues();
+        const {name} = methods.getValues();
         redirectToTopicPath(clusterName, name);
       }
     },
-    [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, getValues],
+    [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods.getValues],
   );
 
   const onSubmit = async (data: TopicFormData) => {
@@ -59,192 +60,168 @@ const New: React.FC<Props> = ({
       </div>
 
       <div className="box">
-        <form onSubmit={handleSubmit(onSubmit)}>
-          <div className="columns">
-            <div className="column is-three-quarters">
-              <label className="label">
-                Topic Name *
-              </label>
-              <input
-                className="input"
-                placeholder="Topic Name"
-                ref={register({
-                  required: 'Topic Name is required.',
-                  pattern: {
-                    value: TOPIC_NAME_VALIDATION_PATTERN,
-                    message: 'Only alphanumeric, _, -, and . allowed',
-                  },
-                })}
-                name="name"
-                autoComplete="off"
-                disabled={isSubmitting}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="name"/>
-              </p>
-            </div>
+        <FormContext {...methods}>
+          <form onSubmit={methods.handleSubmit(onSubmit)}>
+            <div className="columns">
+              <div className="column is-three-quarters">
+                <label className="label">
+                  Topic Name *
+                </label>
+                <input
+                  className="input"
+                  placeholder="Topic Name"
+                  ref={methods.register({
+                    required: 'Topic Name is required.',
+                    pattern: {
+                      value: TOPIC_NAME_VALIDATION_PATTERN,
+                      message: 'Only alphanumeric, _, -, and . allowed',
+                    },
+                  })}
+                  name="name"
+                  autoComplete="off"
+                  disabled={isSubmitting}
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={methods.errors} name="name"/>
+                </p>
+              </div>
 
-            <div className="column">
-              <label className="label">
-                Number of partitions *
-              </label>
-              <input
-                className="input"
-                type="number"
-                placeholder="Number of partitions"
-                defaultValue="1"
-                ref={register({required: 'Number of partitions is required.'})}
-                name="partitions"
-                disabled={isSubmitting}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="partitions"/>
-              </p>
-            </div>
-          </div>
-
-          <div className="columns">
-            <div className="column">
-              <label className="label">
-                Replication Factor *
-              </label>
-              <input
-                className="input"
-                type="number"
-                placeholder="Replication Factor"
-                defaultValue="1"
-                ref={register({required: 'Replication Factor is required.'})}
-                name="replicationFactor"
-                disabled={isSubmitting}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="replicationFactor"/>
-              </p>
+              <div className="column">
+                <label className="label">
+                  Number of partitions *
+                </label>
+                <input
+                  className="input"
+                  type="number"
+                  placeholder="Number of partitions"
+                  defaultValue="1"
+                  ref={methods.register({required: 'Number of partitions is required.'})}
+                  name="partitions"
+                  disabled={isSubmitting}
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={methods.errors} name="partitions"/>
+                </p>
+              </div>
             </div>
 
-            <div className="column">
-              <label className="label">
-                Min In Sync Replicas *
-              </label>
-              <input
-                className="input"
-                type="number"
-                placeholder="Replication Factor"
-                defaultValue="1"
-                ref={register({required: 'Min In Sync Replicas is required.'})}
-                name="minInSyncReplicas"
-                disabled={isSubmitting}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="minInSyncReplicas"/>
-              </p>
-            </div>
-          </div>
-
-          <div className="columns">
-            <div className="column is-one-third">
-              <label className="label">
-                Cleanup policy
-              </label>
-              <div className="select is-block">
-                <select
-                  defaultValue={CleanupPolicy.Delete}
-                  name="cleanupPolicy"
-                  ref={register}
+            <div className="columns">
+              <div className="column">
+                <label className="label">
+                  Replication Factor *
+                </label>
+                <input
+                  className="input"
+                  type="number"
+                  placeholder="Replication Factor"
+                  defaultValue="1"
+                  ref={methods.register({required: 'Replication Factor is required.'})}
+                  name="replicationFactor"
                   disabled={isSubmitting}
-                >
-                  <option value={CleanupPolicy.Delete}>
-                    Delete
-                  </option>
-                  <option value={CleanupPolicy.Compact}>
-                    Compact
-                  </option>
-                </select>
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={methods.errors} name="replicationFactor"/>
+                </p>
               </div>
-            </div>
 
-            <div className="column is-one-third">
-              <label className="label">
-                Time to retain data
-              </label>
-              <div className="select is-block">
-                <select
-                  defaultValue={MILLISECONDS_IN_DAY * 7}
-                  name="retentionMs"
-                  ref={register}
+              <div className="column">
+                <label className="label">
+                  Min In Sync Replicas *
+                </label>
+                <input
+                  className="input"
+                  type="number"
+                  placeholder="Replication Factor"
+                  defaultValue="1"
+                  ref={methods.register({required: 'Min In Sync Replicas is required.'})}
+                  name="minInSyncReplicas"
                   disabled={isSubmitting}
-                >
-                  <option value={MILLISECONDS_IN_DAY / 2}>
-                    12 hours
-                  </option>
-                  <option value={MILLISECONDS_IN_DAY}>
-                    1 day
-                  </option>
-                  <option value={MILLISECONDS_IN_DAY * 2}>
-                    2 days
-                  </option>
-                  <option value={MILLISECONDS_IN_DAY * 7}>
-                    1 week
-                  </option>
-                  <option value={MILLISECONDS_IN_DAY * 7 * 4}>
-                    4 weeks
-                  </option>
-                </select>
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={methods.errors} name="minInSyncReplicas"/>
+                </p>
               </div>
             </div>
 
-            <div className="column is-one-third">
-              <label className="label">
-                Max size on disk in GB
-              </label>
-              <div className="select is-block">
-                <select
-                  defaultValue={-1}
-                  name="retentionBytes"
-                  ref={register}
-                  disabled={isSubmitting}
-                >
-                  <option value={-1}>
-                    Not Set
-                  </option>
-                  <option value={BYTES_IN_GB}>
-                    1 GB
-                  </option>
-                  <option value={BYTES_IN_GB * 10}>
-                    10 GB
-                  </option>
-                  <option value={BYTES_IN_GB * 20}>
-                    20 GB
-                  </option>
-                  <option value={BYTES_IN_GB * 50}>
-                    50 GB
-                  </option>
-                </select>
+            <div className="columns">
+              <div className="column is-one-third">
+                <label className="label">
+                  Cleanup policy
+                </label>
+                <div className="select is-block">
+                  <select
+                    defaultValue={CleanupPolicy.Delete}
+                    name="cleanupPolicy"
+                    ref={methods.register}
+                    disabled={isSubmitting}
+                  >
+                    <option value={CleanupPolicy.Delete}>
+                      Delete
+                    </option>
+                    <option value={CleanupPolicy.Compact}>
+                      Compact
+                    </option>
+                  </select>
+                </div>
+              </div>
+
+              <div className="column is-one-third">
+                <TimeToRetain isSubmitting={isSubmitting} />
+              </div>
+
+              <div className="column is-one-third">
+                <label className="label">
+                  Max size on disk in GB
+                </label>
+                <div className="select is-block">
+                  <select
+                    defaultValue={-1}
+                    name="retentionBytes"
+                    ref={methods.register}
+                    disabled={isSubmitting}
+                  >
+                    <option value={-1}>
+                      Not Set
+                    </option>
+                    <option value={BYTES_IN_GB}>
+                      1 GB
+                    </option>
+                    <option value={BYTES_IN_GB * 10}>
+                      10 GB
+                    </option>
+                    <option value={BYTES_IN_GB * 20}>
+                      20 GB
+                    </option>
+                    <option value={BYTES_IN_GB * 50}>
+                      50 GB
+                    </option>
+                  </select>
+                </div>
               </div>
             </div>
-          </div>
-
-          <div className="columns">
-            <div className="column">
-              <label className="label">
-                Maximum message size in bytes *
-              </label>
-              <input
-                className="input"
-                type="number"
-                defaultValue="1000012"
-                ref={register({required: 'Maximum message size in bytes is required'})}
-                name="maxMessageBytes"
-                disabled={isSubmitting}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="maxMessageBytes"/>
-              </p>
+
+            <div className="columns">
+              <div className="column">
+                <label className="label">
+                  Maximum message size in bytes *
+                </label>
+                <input
+                  className="input"
+                  type="number"
+                  defaultValue="1000012"
+                  ref={methods.register({required: 'Maximum message size in bytes is required'})}
+                  name="maxMessageBytes"
+                  disabled={isSubmitting}
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={methods.errors} name="maxMessageBytes"/>
+                </p>
+              </div>
             </div>
-          </div>
 
-          <input type="submit" className="button is-primary" disabled={isSubmitting}/>
-        </form>
+            <input type="submit" className="button is-primary" disabled={isSubmitting}/>
+          </form>
+        </FormContext>
       </div>
     </div>
   );
diff --git a/kafka-ui-react-app/src/components/Topics/New/TimeToRetain.tsx b/kafka-ui-react-app/src/components/Topics/New/TimeToRetain.tsx
new file mode 100644
index 00000000000..871ce4c3d4c
--- /dev/null
+++ b/kafka-ui-react-app/src/components/Topics/New/TimeToRetain.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import prettyMilliseconds from 'pretty-ms';
+import { useFormContext, ErrorMessage } from 'react-hook-form';
+import { MILLISECONDS_IN_WEEK } from 'lib/constants';
+
+const MILLISECONDS_IN_SECOND = 1000;
+
+interface Props {
+  isSubmitting: boolean;
+}
+
+const TimeToRetain: React.FC<Props> = ({
+  isSubmitting,
+}) => {
+  const { register, errors, watch } = useFormContext();
+  const defaultValue = MILLISECONDS_IN_WEEK;
+  const name: string = 'retentionMs';
+  const watchedValue: any = watch(name, defaultValue.toString());
+
+  const valueHint = React.useMemo(() => {
+    const value = parseInt(watchedValue, 10);
+    return value >= MILLISECONDS_IN_SECOND ? prettyMilliseconds(value) : false;
+  }, [watchedValue])
+
+  return (
+    <>
+      <label className="label">
+        Time to retain data (in ms)
+      </label>
+      <input
+        className="input"
+        type="number"
+        defaultValue={defaultValue}
+        name={name}
+        ref={register(
+          { min: { value: -1, message: 'must be greater than or equal to -1' }}
+        )}
+        disabled={isSubmitting}
+      />
+      <p className="help is-danger">
+        <ErrorMessage errors={errors} name={name}/>
+      </p>
+      {
+        valueHint &&
+        <p className="help is-info">
+          {valueHint}
+        </p>
+      }
+    </>
+  );
+}
+
+export default TimeToRetain;
diff --git a/kafka-ui-react-app/src/lib/constants.ts b/kafka-ui-react-app/src/lib/constants.ts
index 554de1b02f3..ad518d4669a 100644
--- a/kafka-ui-react-app/src/lib/constants.ts
+++ b/kafka-ui-react-app/src/lib/constants.ts
@@ -10,6 +10,6 @@ export const BASE_URL = process.env.REACT_APP_API_URL;
 
 export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
 
-export const MILLISECONDS_IN_DAY = 86_400_000;
+export const MILLISECONDS_IN_WEEK = 604_800_000;
 
 export const BYTES_IN_GB = 1_073_741_824;


Test patch: