Ver Fonte

Merge pull request #20 from provectus/feature/14-add-custom-params-for-topics-creation

Feature/14 add custom params for topics creation
Azat Gataullin há 5 anos atrás
pai
commit
ea9426e8dd

+ 26 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamAction.tsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import CustomParamButton, { CustomParamButtonType } from './CustomParamButton';
+import { isFirstParam } from './CustomParams';
+
+interface Props {
+  index: string;
+  onAdd: (event: React.MouseEvent<HTMLButtonElement>) => void;
+  onRemove: (index: string) => void;
+}
+
+const CustomParamAction: React.FC<Props> = ({
+  index,
+  onAdd,
+  onRemove,
+}) => (
+  <>
+    <label className='label'>&nbsp;</label>
+    {
+      isFirstParam(index)
+        ? <CustomParamButton className="is-success" type={CustomParamButtonType.plus} onClick={onAdd} />
+        : <CustomParamButton className="is-danger" type={CustomParamButtonType.minus} onClick={() => onRemove(index)} />
+    }
+  </>
+)
+
+export default CustomParamAction;

+ 26 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamButton.tsx

@@ -0,0 +1,26 @@
+import React from 'react';
+
+export enum CustomParamButtonType {
+  plus = 'fa-plus',
+  minus = 'fa-minus',
+}
+
+interface Props {
+  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void,
+  className: string;
+  type: CustomParamButtonType;
+}
+
+const CustomParamButton: React.FC<Props> = ({
+  onClick,
+  className,
+  type,
+}) => (
+  <button className={`button ${className} is-outlined`} onClick={onClick}>
+    <span className="icon">
+      <i className={`fas fa-lg ${type}`}></i>
+    </span>
+  </button>
+)
+
+export default CustomParamButton;

+ 20 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamOptions.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { TopicCustomParamOption } from 'redux/interfaces';
+import { CUSTOM_PARAMS_OPTIONS } from './customParamsOptions';
+
+interface Props {};
+
+const CustomParamOptions: React.FC<Props> = () => (
+  <>
+    <option value=''>Select</option>
+    {
+      Object.values(CUSTOM_PARAMS_OPTIONS).map((opt: TopicCustomParamOption) => (
+        <option key={opt.name} value={opt.name}>
+          {opt.name}
+        </option>
+      ))
+    }
+  </>
+);
+
+export default React.memo(CustomParamOptions);

+ 67 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamSelect.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { useFormContext, ErrorMessage } from 'react-hook-form';
+import CustomParamOptions from './CustomParamOptions';
+import { isFirstParam, INDEX_PREFIX } from './CustomParams';
+import { TopicFormCustomParam } from 'redux/interfaces';
+
+interface Props {
+  isDisabled: boolean;
+  index: string;
+  name: string;
+}
+
+const CustomParamSelect: React.FC<Props> = ({
+  isDisabled,
+  index,
+  name,
+}) => {
+
+  const { register, unregister, errors, getValues, triggerValidation } = useFormContext();
+  const optInputName = `${index}[name]`;
+
+  React.useEffect(
+    () => { if (isFirstParam(index)) { unregister(optInputName) } },
+  );
+
+  const selectedMustBeUniq = (selected: string) => {
+    const values = getValues({ nest: true });
+    const customParamsValues: TopicFormCustomParam = values.customParams;
+
+    let valid = true;
+
+    for (const [key, customParam] of Object.entries(customParamsValues)) {
+      if (`${INDEX_PREFIX}.${key}` === index) { continue; }
+      if (selected === customParam.name) {
+        valid = false;
+        break;
+      };
+    }
+
+    return valid ? true : 'Custom Parameter must be unique';
+  };
+
+  return (
+    <>
+      <label className="label">Custom Parameter</label>
+      <div className="select is-block">
+        <select
+          name={optInputName}
+          ref={register({
+            required: 'Custom Parameter is required.',
+            validate: { unique: selected => selectedMustBeUniq(selected) },
+          })}
+          onChange={() => triggerValidation(optInputName)}
+          disabled={isDisabled}
+          defaultValue={name}
+        >
+          <CustomParamOptions />
+        </select>
+        <p className="help is-danger">
+          <ErrorMessage errors={errors} name={optInputName}/>
+        </p>
+      </div>
+    </>
+  );
+};
+
+export default React.memo(CustomParamSelect);

+ 59 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamValue.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import { useFormContext, ErrorMessage } from 'react-hook-form';
+import { CUSTOM_PARAMS_OPTIONS } from './customParamsOptions';
+import { isFirstParam } from './CustomParams';
+
+interface Props {
+  isDisabled: boolean;
+  index: string;
+  name: string;
+  defaultValue: string;
+}
+
+const CustomParamValue: React.FC<Props> = ({
+  isDisabled,
+  index,
+  name,
+  defaultValue,
+}) => {
+
+  const { register, unregister, errors, watch, setValue } = useFormContext();
+  const selectInputName: string = `${index}[name]`;
+  const valInputName: string    = `${index}[value]`;
+  const selectedParamName = watch(selectInputName, name);
+
+  React.useEffect(
+    () => {
+      if (selectedParamName) {
+        setValue(valInputName, CUSTOM_PARAMS_OPTIONS[selectedParamName].defaultValue, true);
+      }
+    },
+    [selectedParamName],
+  );
+
+  React.useEffect(
+    () => { if (isFirstParam(index)) { unregister(valInputName) } },
+  );
+
+  return (
+    <>
+      <label className="label">Value</label>
+      <input
+        className="input"
+        placeholder="Value"
+        ref={register({
+          required: 'Value is required.',
+        })}
+        name={valInputName}
+        defaultValue={defaultValue}
+        autoComplete="off"
+        disabled={isDisabled}
+      />
+      <p className="help is-danger">
+        <ErrorMessage errors={errors} name={valInputName}/>
+      </p>
+    </>
+  );
+};
+
+export default React.memo(CustomParamValue);

+ 89 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParams.tsx

@@ -0,0 +1,89 @@
+import React from 'react';
+import { omit, reject } from 'lodash';
+
+import { TopicFormCustomParams } from 'redux/interfaces';
+import CustomParamSelect from './CustomParamSelect';
+import CustomParamValue from './CustomParamValue';
+import CustomParamAction from './CustomParamAction';
+
+const DEFAULT_INDEX = 'default';
+export const INDEX_PREFIX = 'customParams';
+export const isFirstParam = (index: string) => (index === DEFAULT_INDEX);
+
+interface Props {
+  isSubmitting: boolean;
+}
+
+const CustomParams: React.FC<Props> = ({
+  isSubmitting,
+}) => {
+
+  const [formCustomParams, setFormCustomParams] = React.useState<TopicFormCustomParams>({
+    byIndex: { [DEFAULT_INDEX]: { name: '', value: '' } },
+    allIndexes: [DEFAULT_INDEX],
+  });
+
+  const onAdd = (event: React.MouseEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+
+    const newIndex = `${INDEX_PREFIX}.${new Date().getTime()}`;
+
+    setFormCustomParams({
+      ...formCustomParams,
+      byIndex: {
+        ...formCustomParams.byIndex,
+        [newIndex]: { name: '', value: '' },
+      },
+      allIndexes: [
+        formCustomParams.allIndexes[0],
+        newIndex,
+        ...formCustomParams.allIndexes.slice(1),
+      ],
+    });
+  }
+
+  const onRemove = (index: string) => {
+    setFormCustomParams({
+      ...formCustomParams,
+      byIndex: omit(formCustomParams.byIndex, index),
+      allIndexes: reject(formCustomParams.allIndexes, (i) => (i === index)),
+    });
+  }
+
+  return (
+    <>
+      {
+        formCustomParams.allIndexes.map((index) => (
+          <div className="columns is-centered" key={index}>
+            <div className="column">
+              <CustomParamSelect
+                index={index}
+                isDisabled={isFirstParam(index) || isSubmitting}
+                name={formCustomParams.byIndex[index].name}
+              />
+            </div>
+
+            <div className="column">
+              <CustomParamValue
+                index={index}
+                isDisabled={isFirstParam(index) || isSubmitting}
+                name={formCustomParams.byIndex[index].name}
+                defaultValue={formCustomParams.byIndex[index].value}
+              />
+            </div>
+
+            <div className="column is-narrow">
+              <CustomParamAction
+                index={index}
+                onAdd={onAdd}
+                onRemove={onRemove}
+              />
+            </div>
+          </div>
+        ))
+      }
+    </>
+  );
+};
+
+export default CustomParams;

+ 18 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamsContainer.tsx

@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import { RootState } from 'redux/interfaces';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import CustomParams from './CustomParams';
+
+interface RouteProps {};
+
+interface OwnProps extends RouteComponentProps<RouteProps> {
+  isSubmitting: boolean;
+}
+
+const mapStateToProps = (state: RootState, { isSubmitting }: OwnProps) => ({
+  isSubmitting,
+})
+
+export default withRouter(
+  connect(mapStateToProps)(CustomParams)
+);

+ 96 - 0
kafka-ui-react-app/src/components/Topics/New/CustomParams/customParamsOptions.tsx

@@ -0,0 +1,96 @@
+import { TopicCustomParamOption } from 'redux/interfaces';
+
+interface CustomParamOption {
+  [optionName: string]: TopicCustomParamOption;
+}
+
+export const CUSTOM_PARAMS_OPTIONS: CustomParamOption = {
+  "compression.type": {
+    "name": "compression.type",
+    "defaultValue": "producer"
+  },
+  "leader.replication.throttled.replicas": {
+    "name": "leader.replication.throttled.replicas",
+    "defaultValue": ""
+  },
+  "message.downconversion.enable": {
+    "name": "message.downconversion.enable",
+    "defaultValue": "true"
+  },
+  "segment.jitter.ms": {
+    "name": "segment.jitter.ms",
+    "defaultValue": "0"
+  },
+  "flush.ms": {
+    "name": "flush.ms",
+    "defaultValue": "9223372036854775807"
+  },
+  "follower.replication.throttled.replicas": {
+    "name": "follower.replication.throttled.replicas",
+    "defaultValue": ""
+  },
+  "segment.bytes": {
+    "name": "segment.bytes",
+    "defaultValue": "1073741824"
+  },
+  "flush.messages": {
+    "name": "flush.messages",
+    "defaultValue": "9223372036854775807"
+  },
+  "message.format.version": {
+    "name": "message.format.version",
+    "defaultValue": "2.3-IV1"
+  },
+  "file.delete.delay.ms": {
+    "name": "file.delete.delay.ms",
+    "defaultValue": "60000"
+  },
+  "max.compaction.lag.ms": {
+    "name": "max.compaction.lag.ms",
+    "defaultValue": "9223372036854775807"
+  },
+  "min.compaction.lag.ms": {
+    "name": "min.compaction.lag.ms",
+    "defaultValue": "0"
+  },
+  "message.timestamp.type": {
+    "name": "message.timestamp.type",
+    "defaultValue": "CreateTime"
+  },
+  "preallocate": {
+    "name": "preallocate",
+    "defaultValue": "false"
+  },
+  "min.cleanable.dirty.ratio": {
+    "name": "min.cleanable.dirty.ratio",
+    "defaultValue": "0.5"
+  },
+  "index.interval.bytes": {
+    "name": "index.interval.bytes",
+    "defaultValue": "4096"
+  },
+  "unclean.leader.election.enable": {
+    "name": "unclean.leader.election.enable",
+    "defaultValue": "true"
+  },
+  "retention.bytes": {
+    "name": "retention.bytes",
+    "defaultValue": "-1"
+  },
+  "delete.retention.ms": {
+    "name": "delete.retention.ms",
+    "defaultValue": "86400000"
+  },
+  "segment.ms": {
+    "name": "segment.ms",
+    "defaultValue": "604800000"
+  },
+  "message.timestamp.difference.max.ms": {
+    "name": "message.timestamp.difference.max.ms",
+    "defaultValue": "9223372036854775807"
+  },
+  "segment.index.bytes": {
+    "name": "segment.index.bytes",
+    "defaultValue": "10485760"
+  }
+}

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

@@ -1,14 +1,16 @@
 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, FormContext, ErrorMessage } from 'react-hook-form';
 
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import CustomParamsContainer from "./CustomParams/CustomParamsContainer";
+import TimeToRetain from './TimeToRetain';
+import { clusterTopicsPath } from 'lib/paths';
 import {
   TOPIC_NAME_VALIDATION_PATTERN,
   BYTES_IN_GB,
 } from 'lib/constants';
-import TimeToRetain from './TimeToRetain';
+
 
 interface Props {
   clusterName: ClusterName;
@@ -219,6 +221,8 @@ const New: React.FC<Props> = ({
               </div>
             </div>
 
+            <CustomParamsContainer isSubmitting={isSubmitting} />
+
             <input type="submit" className="button is-primary" disabled={isSubmitting}/>
           </form>
         </FormContext>

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

@@ -1,4 +1,4 @@
-import { createAsyncAction} from 'typesafe-actions';
+import { createAsyncAction } from 'typesafe-actions';
 import { ActionType } from 'redux/actionType';
 import { ConsumerGroup } from '../interfaces/consumerGroup';
 import {

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

@@ -23,6 +23,11 @@ export interface TopicPartition {
   replicas: TopicReplica[];
 }
 
+export interface TopicCustomParamOption {
+  name: string;
+  defaultValue: string;
+}
+
 export interface TopicDetails {
   partitionCount?: number;
   replicationFactor?: number;
@@ -39,13 +44,23 @@ export interface Topic {
   partitions: TopicPartition[];
 }
 
+export interface TopicFormCustomParam {
+  name: string;
+  value: string;
+}
+
+export interface TopicFormCustomParams {
+  byIndex: { [paramIndex: string]: TopicFormCustomParam };
+  allIndexes: string[];
+}
+
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
   config?: TopicConfig[];
 }
 
 export interface TopicsState {
-  byName: { [topicName: string]: TopicWithDetailedInfo },
-  allNames: TopicName[],
+  byName: { [topicName: string]: TopicWithDetailedInfo };
+  allNames: TopicName[];
 }
 
 export interface TopicFormData {
@@ -57,4 +72,7 @@ export interface TopicFormData {
   retentionMs: number;
   retentionBytes: number;
   maxMessageBytes: number;
+  customParams: {
+    [index: string]: TopicFormCustomParam;
+  };
 };