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

View file

@ -2,11 +2,20 @@ import { connect } from 'react-redux';
import { RootState } from 'types'; import { RootState } from 'types';
import { getTopicList, getExternalTopicList } from 'redux/reducers/topics/selectors'; import { getTopicList, getExternalTopicList } from 'redux/reducers/topics/selectors';
import List from './List'; 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), topics: getTopicList(state),
externalTopics: getExternalTopicList(state), externalTopics: getExternalTopicList(state),
}); });
export default withRouter(
export default connect(mapStateToProps)(List); 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 React from 'react';
import { ClusterId } from 'types';
import { import {
Switch, Switch,
Route, Route,
@ -6,7 +7,7 @@ import {
import ListContainer from './List/ListContainer'; import ListContainer from './List/ListContainer';
import DetailsContainer from './Details/DetailsContainer'; import DetailsContainer from './Details/DetailsContainer';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import { ClusterId } from 'types'; import NewContainer from './New/NewContainer';
interface Props { interface Props {
clusterId: string; clusterId: string;
@ -26,6 +27,7 @@ const Topics: React.FC<Props> = ({
return ( return (
<Switch> <Switch>
<Route exact path="/clusters/:clusterId/topics" component={ListContainer} /> <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} /> <Route path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
</Switch> </Switch>
); );

View file

@ -7,3 +7,7 @@ export const BASE_PARAMS: RequestInit = {
}; };
export const BASE_URL = 'http://localhost:3004'; 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 clusterBrokersPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/brokers`;
export const clusterTopicsPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/topics`; 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 clusterTopicPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}`;
export const clusterTopicSettingsPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}/settings`; export const clusterTopicSettingsPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}/settings`;

View file

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

View file

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