[UI] Topic Creation Page

This commit is contained in:
Oleg Shuralev 2020-01-17 22:35:19 +03:00
parent 3ef98359d5
commit e24423e613
No known key found for this signature in database
GPG key ID: 0459DF80E1A2FD1B
9 changed files with 307 additions and 17 deletions

View file

@ -1,14 +1,18 @@
import React from 'react';
import { TopicWithDetailedInfo } from 'types';
import { TopicWithDetailedInfo, ClusterId } from 'types';
import ListItem from './ListItem';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { NavLink } from 'react-router-dom';
import { clusterTopicNewPath } from 'lib/paths';
interface Props {
clusterId: ClusterId;
topics: (TopicWithDetailedInfo)[];
externalTopics: (TopicWithDetailedInfo)[];
}
const List: React.FC<Props> = ({
clusterId,
topics,
externalTopics,
}) => {
@ -23,18 +27,30 @@ const List: React.FC<Props> = ({
<Breadcrumb>All Topics</Breadcrumb>
<div className="box">
<div className="field">
<input
id="switchRoundedDefault"
type="checkbox"
name="switchRoundedDefault"
className="switch is-rounded"
checked={showInternal}
onChange={handleSwitch}
/>
<label htmlFor="switchRoundedDefault">
Show Internal Topics
</label>
<div className="level">
<div className="level-item level-left">
<div className="field">
<input
id="switchRoundedDefault"
type="checkbox"
name="switchRoundedDefault"
className="switch is-rounded"
checked={showInternal}
onChange={handleSwitch}
/>
<label htmlFor="switchRoundedDefault">
Show Internal Topics
</label>
</div>
</div>
<div className="level-item level-right">
<NavLink
className="button is-primary"
to={clusterTopicNewPath(clusterId)}
>
Add a Topic
</NavLink>
</div>
</div>
</div>
<div className="box">

View file

@ -2,11 +2,20 @@ import { connect } from 'react-redux';
import { RootState } from 'types';
import { getTopicList, getExternalTopicList } from 'redux/reducers/topics/selectors';
import List from './List';
import { withRouter, RouteComponentProps } from 'react-router-dom';
const mapStateToProps = (state: RootState) => ({
interface RouteProps {
clusterId: string;
}
interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { clusterId } } }: OwnProps) => ({
clusterId,
topics: getTopicList(state),
externalTopics: getExternalTopicList(state),
});
export default connect(mapStateToProps)(List);
export default withRouter(
connect(mapStateToProps)(List)
);

View file

@ -0,0 +1,218 @@
import React from 'react';
import { ClusterId, CleanupPolicy, TopicFormData } from 'types';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterTopicsPath } from 'lib/paths';
import { useForm, ErrorMessage } from 'react-hook-form';
import {
TOPIC_NAME_VALIDATION_PATTERN,
MILLISECONDS_IN_DAY,
BYTES_IN_GB,
} from 'lib/constants';
interface Props {
clusterId: ClusterId;
}
const New: React.FC<Props> = ({
clusterId,
}) => {
const { register, handleSubmit, errors } = useForm<TopicFormData>(); // initialise the hook
const onSubmit = (data: TopicFormData) => {
console.log(data);
};
return (
<div className="section">
<div className="level">
<div className="level-item level-left">
<Breadcrumb links={[
{ href: clusterTopicsPath(clusterId), label: 'All Topics' },
]}>
New Topic
</Breadcrumb>
</div>
</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"
/>
<p className="help is-danger">
<ErrorMessage errors={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"
/>
<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"
/>
<p className="help is-danger">
<ErrorMessage errors={errors} name="replicationFactor" />
</p>
</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"
/>
<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">
<option value={CleanupPolicy.Delete}>
Delete
</option>
<option value={CleanupPolicy.Compact}>
Compact
</option>
</select>
</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}
>
<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>
</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}
>
<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 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"
/>
<p className="help is-danger">
<ErrorMessage errors={errors} name="maxMessageBytes" />
</p>
</div>
</div>
<input type="submit" className="button is-primary"/>
</form>
</div>
</div>
);
}
export default New;

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { RootState } from 'types';
import New from './New';
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface RouteProps {
clusterId: string;
}
interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { clusterId } } }: OwnProps) => ({
clusterId,
});
export default withRouter(
connect(mapStateToProps)(New)
);

View file

@ -1,4 +1,5 @@
import React from 'react';
import { ClusterId } from 'types';
import {
Switch,
Route,
@ -6,7 +7,7 @@ import {
import ListContainer from './List/ListContainer';
import DetailsContainer from './Details/DetailsContainer';
import PageLoader from 'components/common/PageLoader/PageLoader';
import { ClusterId } from 'types';
import NewContainer from './New/NewContainer';
interface Props {
clusterId: string;
@ -26,6 +27,7 @@ const Topics: React.FC<Props> = ({
return (
<Switch>
<Route exact path="/clusters/:clusterId/topics" component={ListContainer} />
<Route exact path="/clusters/:clusterId/topics/new" component={NewContainer} />
<Route path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
</Switch>
);

View file

@ -7,3 +7,7 @@ export const BASE_PARAMS: RequestInit = {
};
export const BASE_URL = 'http://localhost:3004';
export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
export const MILLISECONDS_IN_DAY = 86_400_000;
export const BYTES_IN_GB = 1_073_741_824;

View file

@ -5,6 +5,7 @@ const clusterPath = (clusterId: ClusterId) => `/clusters/${clusterId}`;
export const clusterBrokersPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/brokers`;
export const clusterTopicsPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/topics`;
export const clusterTopicNewPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/topics/new`;
export const clusterTopicPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}`;
export const clusterTopicSettingsPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}/settings`;

View file

@ -28,6 +28,10 @@
animation: fadein .5s;
}
.select.is-block select {
width: 100%;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }

View file

@ -1,4 +1,10 @@
export type TopicName = string;
export enum CleanupPolicy {
Delete = 'delete',
Compact = 'compact',
}
export interface TopicConfig {
name: string;
value: string;
@ -41,3 +47,14 @@ export interface TopicsState {
byName: { [topicName: string]: TopicWithDetailedInfo },
allNames: TopicName[],
}
export interface TopicFormData {
name: string;
partitions: number;
replicationFactor: number;
minInSyncReplicas: number;
cleanupPolicy: string;
retentionMs: number;
retentionBytes: number;
maxMessageBytes: number;
};