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

Feature/14 add custom params for topics creation
This commit is contained in:
Azat Gataullin 2020-04-13 12:20:44 +03:00 committed by GitHub
commit ea9426e8dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 429 additions and 6 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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)
);

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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 {

View file

@ -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;
};
};