[UI] Topic Creation Page
This commit is contained in:
parent
3ef98359d5
commit
e24423e613
9 changed files with 307 additions and 17 deletions
|
@ -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,6 +27,8 @@ const List: React.FC<Props> = ({
|
||||||
<Breadcrumb>All Topics</Breadcrumb>
|
<Breadcrumb>All Topics</Breadcrumb>
|
||||||
|
|
||||||
<div className="box">
|
<div className="box">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-item level-left">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<input
|
<input
|
||||||
id="switchRoundedDefault"
|
id="switchRoundedDefault"
|
||||||
|
@ -37,6 +43,16 @@ const List: React.FC<Props> = ({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="box">
|
||||||
<table className="table is-striped is-fullwidth">
|
<table className="table is-striped is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
218
frontend/src/components/Topics/New/New.tsx
Normal file
218
frontend/src/components/Topics/New/New.tsx
Normal 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;
|
19
frontend/src/components/Topics/New/NewContainer.ts
Normal file
19
frontend/src/components/Topics/New/NewContainer.ts
Normal 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)
|
||||||
|
);
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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`;
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue