diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamAction.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamAction.tsx new file mode 100644 index 00000000000..f634e2a65fc --- /dev/null +++ b/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) => void; + onRemove: (index: string) => void; +} + +const CustomParamAction: React.FC = ({ + index, + onAdd, + onRemove, +}) => ( + <> + + { + isFirstParam(index) + ? + : onRemove(index)} /> + } + +) + +export default CustomParamAction; diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamButton.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamButton.tsx new file mode 100644 index 00000000000..c9fe3d8cd07 --- /dev/null +++ b/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) => void, + className: string; + type: CustomParamButtonType; +} + +const CustomParamButton: React.FC = ({ + onClick, + className, + type, +}) => ( + +) + +export default CustomParamButton; diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamOptions.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamOptions.tsx new file mode 100644 index 00000000000..c2a08cd3f74 --- /dev/null +++ b/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 = () => ( + <> + + { + Object.values(CUSTOM_PARAMS_OPTIONS).map((opt: TopicCustomParamOption) => ( + + )) + } + +); + +export default React.memo(CustomParamOptions); diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamSelect.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamSelect.tsx new file mode 100644 index 00000000000..70177be7274 --- /dev/null +++ b/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 = ({ + 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 ( + <> + +
+ +

+ +

+
+ + ); +}; + +export default React.memo(CustomParamSelect); diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamValue.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamValue.tsx new file mode 100644 index 00000000000..9d928907d72 --- /dev/null +++ b/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 = ({ + 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 ( + <> + + +

+ +

+ + ); +}; + +export default React.memo(CustomParamValue); diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParams.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParams.tsx new file mode 100644 index 00000000000..8fe22db0368 --- /dev/null +++ b/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 = ({ + isSubmitting, +}) => { + + const [formCustomParams, setFormCustomParams] = React.useState({ + byIndex: { [DEFAULT_INDEX]: { name: '', value: '' } }, + allIndexes: [DEFAULT_INDEX], + }); + + const onAdd = (event: React.MouseEvent) => { + 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) => ( +
+
+ +
+ +
+ +
+ +
+ +
+
+ )) + } + + ); +}; + +export default CustomParams; diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamsContainer.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/CustomParamsContainer.tsx new file mode 100644 index 00000000000..2e9a1867537 --- /dev/null +++ b/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 { + isSubmitting: boolean; +} + +const mapStateToProps = (state: RootState, { isSubmitting }: OwnProps) => ({ + isSubmitting, +}) + +export default withRouter( + connect(mapStateToProps)(CustomParams) +); diff --git a/kafka-ui-react-app/src/components/Topics/New/CustomParams/customParamsOptions.tsx b/kafka-ui-react-app/src/components/Topics/New/CustomParams/customParamsOptions.tsx new file mode 100644 index 00000000000..d03de0581c2 --- /dev/null +++ b/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" + } +} 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 51b53f6078b..c782ca75b82 100644 --- a/kafka-ui-react-app/src/components/Topics/New/New.tsx +++ b/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 = ({ + + diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index 449328e9e40..6ac3085b610 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/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 { diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index 47c381b2d70..465c0522fb0 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/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; + }; };