Enhancement/improve time to retain usability v2 (#35)

* enhancement/improve-time-to-retain-usability

* add-btn-controls-for-time-to-retain-for-topics
This commit is contained in:
Azat Gataullin 2020-04-28 14:21:36 +03:00 committed by GitHub
parent 3c5b46bd76
commit 2d45c488f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 168 additions and 103 deletions

View file

@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
import { ClusterName, CleanupPolicy, TopicFormData, TopicName } from 'redux/interfaces'; import {
ClusterName,
CleanupPolicy,
TopicFormData,
TopicName,
} from 'redux/interfaces';
import { useForm, FormContext, ErrorMessage } from 'react-hook-form'; import { useForm, FormContext, ErrorMessage } from 'react-hook-form';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import CustomParamsContainer from "./CustomParams/CustomParamsContainer";
import TimeToRetain from './TimeToRetain';
import { clusterTopicsPath } from 'lib/paths'; import { clusterTopicsPath } from 'lib/paths';
import { import { TOPIC_NAME_VALIDATION_PATTERN, BYTES_IN_GB } from 'lib/constants';
TOPIC_NAME_VALIDATION_PATTERN, import CustomParamsContainer from './CustomParams/CustomParamsContainer';
BYTES_IN_GB, import TimeToRetain from './TimeToRetain';
} from 'lib/constants';
interface Props { interface Props {
clusterName: ClusterName; clusterName: ClusterName;
@ -25,20 +26,23 @@ const New: React.FC<Props> = ({
isTopicCreated, isTopicCreated,
createTopic, createTopic,
redirectToTopicPath, redirectToTopicPath,
resetUploadedState resetUploadedState,
}) => { }) => {
const methods = useForm<TopicFormData>(); const methods = useForm<TopicFormData>();
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false); const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
React.useEffect( React.useEffect(() => {
() => {
if (isSubmitting && isTopicCreated) { if (isSubmitting && isTopicCreated) {
const { name } = methods.getValues(); const { name } = methods.getValues();
redirectToTopicPath(clusterName, name); redirectToTopicPath(clusterName, name);
} }
}, }, [
[isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods.getValues], isSubmitting,
); isTopicCreated,
redirectToTopicPath,
clusterName,
methods.getValues,
]);
const onSubmit = async (data: TopicFormData) => { 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 // TODO: need to fix loader. After success loading the first time, we won't wait for creation any more, because state is
@ -53,9 +57,11 @@ const New: React.FC<Props> = ({
<div className="section"> <div className="section">
<div className="level"> <div className="level">
<div className="level-item level-left"> <div className="level-item level-left">
<Breadcrumb links={[ <Breadcrumb
links={[
{ href: clusterTopicsPath(clusterName), label: 'All Topics' }, { href: clusterTopicsPath(clusterName), label: 'All Topics' },
]}> ]}
>
New Topic New Topic
</Breadcrumb> </Breadcrumb>
</div> </div>
@ -66,9 +72,7 @@ const New: React.FC<Props> = ({
<form onSubmit={methods.handleSubmit(onSubmit)}> <form onSubmit={methods.handleSubmit(onSubmit)}>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column is-three-quarters">
<label className="label"> <label className="label">Topic Name *</label>
Topic Name *
</label>
<input <input
className="input" className="input"
placeholder="Topic Name" placeholder="Topic Name"
@ -89,15 +93,15 @@ const New: React.FC<Props> = ({
</div> </div>
<div className="column"> <div className="column">
<label className="label"> <label className="label">Number of partitions *</label>
Number of partitions *
</label>
<input <input
className="input" className="input"
type="number" type="number"
placeholder="Number of partitions" placeholder="Number of partitions"
defaultValue="1" defaultValue="1"
ref={methods.register({required: 'Number of partitions is required.'})} ref={methods.register({
required: 'Number of partitions is required.',
})}
name="partitions" name="partitions"
disabled={isSubmitting} disabled={isSubmitting}
/> />
@ -109,47 +113,51 @@ const New: React.FC<Props> = ({
<div className="columns"> <div className="columns">
<div className="column"> <div className="column">
<label className="label"> <label className="label">Replication Factor *</label>
Replication Factor *
</label>
<input <input
className="input" className="input"
type="number" type="number"
placeholder="Replication Factor" placeholder="Replication Factor"
defaultValue="1" defaultValue="1"
ref={methods.register({required: 'Replication Factor is required.'})} ref={methods.register({
required: 'Replication Factor is required.',
})}
name="replicationFactor" name="replicationFactor"
disabled={isSubmitting} disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={methods.errors} name="replicationFactor"/> <ErrorMessage
errors={methods.errors}
name="replicationFactor"
/>
</p> </p>
</div> </div>
<div className="column"> <div className="column">
<label className="label"> <label className="label">Min In Sync Replicas *</label>
Min In Sync Replicas *
</label>
<input <input
className="input" className="input"
type="number" type="number"
placeholder="Replication Factor" placeholder="Replication Factor"
defaultValue="1" defaultValue="1"
ref={methods.register({required: 'Min In Sync Replicas is required.'})} ref={methods.register({
required: 'Min In Sync Replicas is required.',
})}
name="minInSyncReplicas" name="minInSyncReplicas"
disabled={isSubmitting} disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={methods.errors} name="minInSyncReplicas"/> <ErrorMessage
errors={methods.errors}
name="minInSyncReplicas"
/>
</p> </p>
</div> </div>
</div> </div>
<div className="columns"> <div className="columns">
<div className="column is-one-third"> <div className="column is-one-third">
<label className="label"> <label className="label">Cleanup policy</label>
Cleanup policy
</label>
<div className="select is-block"> <div className="select is-block">
<select <select
defaultValue={CleanupPolicy.Delete} defaultValue={CleanupPolicy.Delete}
@ -157,12 +165,8 @@ const New: React.FC<Props> = ({
ref={methods.register} ref={methods.register}
disabled={isSubmitting} disabled={isSubmitting}
> >
<option value={CleanupPolicy.Delete}> <option value={CleanupPolicy.Delete}>Delete</option>
Delete <option value={CleanupPolicy.Compact}>Compact</option>
</option>
<option value={CleanupPolicy.Compact}>
Compact
</option>
</select> </select>
</div> </div>
</div> </div>
@ -172,9 +176,7 @@ const New: React.FC<Props> = ({
</div> </div>
<div className="column is-one-third"> <div className="column is-one-third">
<label className="label"> <label className="label">Max size on disk in GB</label>
Max size on disk in GB
</label>
<div className="select is-block"> <div className="select is-block">
<select <select
defaultValue={-1} defaultValue={-1}
@ -182,21 +184,11 @@ const New: React.FC<Props> = ({
ref={methods.register} ref={methods.register}
disabled={isSubmitting} disabled={isSubmitting}
> >
<option value={-1}> <option value={-1}>Not Set</option>
Not Set <option value={BYTES_IN_GB}>1 GB</option>
</option> <option value={BYTES_IN_GB * 10}>10 GB</option>
<option value={BYTES_IN_GB}> <option value={BYTES_IN_GB * 20}>20 GB</option>
1 GB <option value={BYTES_IN_GB * 50}>50 GB</option>
</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> </select>
</div> </div>
</div> </div>
@ -204,26 +196,33 @@ const New: React.FC<Props> = ({
<div className="columns"> <div className="columns">
<div className="column"> <div className="column">
<label className="label"> <label className="label">Maximum message size in bytes *</label>
Maximum message size in bytes *
</label>
<input <input
className="input" className="input"
type="number" type="number"
defaultValue="1000012" defaultValue="1000012"
ref={methods.register({required: 'Maximum message size in bytes is required'})} ref={methods.register({
required: 'Maximum message size in bytes is required',
})}
name="maxMessageBytes" name="maxMessageBytes"
disabled={isSubmitting} disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={methods.errors} name="maxMessageBytes"/> <ErrorMessage
errors={methods.errors}
name="maxMessageBytes"
/>
</p> </p>
</div> </div>
</div> </div>
<CustomParamsContainer isSubmitting={isSubmitting} /> <CustomParamsContainer isSubmitting={isSubmitting} />
<input type="submit" className="button is-primary" disabled={isSubmitting}/> <input
type="submit"
className="button is-primary"
disabled={isSubmitting}
/>
</form> </form>
</FormContext> </FormContext>
</div> </div>

View file

@ -1,53 +1,52 @@
import React from 'react'; import React from 'react';
import prettyMilliseconds from 'pretty-ms'; import prettyMilliseconds from 'pretty-ms';
import { useFormContext, ErrorMessage } from 'react-hook-form'; import { useFormContext, ErrorMessage } from 'react-hook-form';
import { MILLISECONDS_IN_WEEK } from 'lib/constants'; import { MILLISECONDS_IN_WEEK, MILLISECONDS_IN_SECOND } from 'lib/constants';
import TimeToRetainBtns from './TimeToRetainBtns';
const MILLISECONDS_IN_SECOND = 1000;
interface Props { interface Props {
isSubmitting: boolean; isSubmitting: boolean;
} }
const TimeToRetain: React.FC<Props> = ({ const TimeToRetain: React.FC<Props> = ({ isSubmitting }) => {
isSubmitting,
}) => {
const { register, errors, watch } = useFormContext(); const { register, errors, watch } = useFormContext();
const defaultValue = MILLISECONDS_IN_WEEK; const defaultValue = MILLISECONDS_IN_WEEK;
const name: string = 'retentionMs'; const name = 'retentionMs';
const watchedValue: any = watch(name, defaultValue.toString()); const watchedValue = watch(name, defaultValue.toString());
const valueHint = React.useMemo(() => { const valueHint = React.useMemo(() => {
const value = parseInt(watchedValue, 10); const value = parseInt(watchedValue, 10);
return value >= MILLISECONDS_IN_SECOND ? prettyMilliseconds(value) : false; return value >= MILLISECONDS_IN_SECOND ? prettyMilliseconds(value) : false;
}, [watchedValue]) }, [watchedValue]);
return ( return (
<> <>
<label className="label"> <label
Time to retain data (in ms) className="label is-flex"
style={{ justifyContent: 'space-between' }}
>
<div>Time to retain data (in ms)</div>
{valueHint && <span className="has-text-info">{valueHint}</span>}
</label> </label>
<input <input
className="input" className="input"
id="timeToRetain"
type="number" type="number"
defaultValue={defaultValue} defaultValue={defaultValue}
name={name} name={name}
ref={register( ref={register({
{ min: { value: -1, message: 'must be greater than or equal to -1' }} min: { value: -1, message: 'must be greater than or equal to -1' },
)} })}
disabled={isSubmitting} disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name={name} /> <ErrorMessage errors={errors} name={name} />
</p> </p>
{
valueHint && <TimeToRetainBtns name={name} value={watchedValue} />
<p className="help is-info">
{valueHint}
</p>
}
</> </>
); );
} };
export default TimeToRetain; export default TimeToRetain;

View file

@ -0,0 +1,29 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import cx from 'classnames';
import { MILLISECONDS_IN_WEEK } from 'lib/constants';
interface Props {
inputName: string;
text: string;
value: number;
}
const TimeToRetainBtn: React.FC<Props> = ({ inputName, text, value }) => {
const { setValue, watch } = useFormContext();
const watchedValue = watch(inputName, MILLISECONDS_IN_WEEK.toString());
return (
<button
type="button"
className={cx('button', {
'is-info': watchedValue === value.toString(),
})}
onClick={() => setValue(inputName, value)}
>
{text}
</button>
);
};
export default TimeToRetainBtn;

View file

@ -0,0 +1,36 @@
import React from 'react';
import { MILLISECONDS_IN_DAY } from 'lib/constants';
import TimeToRetainBtn from './TimeToRetainBtn';
interface Props {
name: string;
value: string;
}
const TimeToRetainBtns: React.FC<Props> = ({ name }) => (
<div className="buttons are-small">
<TimeToRetainBtn
text="12h"
inputName={name}
value={MILLISECONDS_IN_DAY / 2}
/>
<TimeToRetainBtn text="1d" inputName={name} value={MILLISECONDS_IN_DAY} />
<TimeToRetainBtn
text="2d"
inputName={name}
value={MILLISECONDS_IN_DAY * 2}
/>
<TimeToRetainBtn
text="7d"
inputName={name}
value={MILLISECONDS_IN_DAY * 7}
/>
<TimeToRetainBtn
text="4w"
inputName={name}
value={MILLISECONDS_IN_DAY * 7 * 24}
/>
</div>
);
export default TimeToRetainBtns;

View file

@ -11,5 +11,7 @@ export const BASE_URL = process.env.REACT_APP_API_URL;
export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/); export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
export const MILLISECONDS_IN_WEEK = 604_800_000; export const MILLISECONDS_IN_WEEK = 604_800_000;
export const MILLISECONDS_IN_DAY = 86_400_000;
export const MILLISECONDS_IN_SECOND = 1_000;
export const BYTES_IN_GB = 1_073_741_824; export const BYTES_IN_GB = 1_073_741_824;