[UI] Create Topic action

This commit is contained in:
Oleg Shuralev 2020-01-17 23:53:46 +03:00
parent ee92f4482f
commit 82d81dd847
No known key found for this signature in database
GPG key ID: 0459DF80E1A2FD1B
7 changed files with 114 additions and 11 deletions

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ClusterId, CleanupPolicy, TopicFormData } from 'types'; import { ClusterId, CleanupPolicy, TopicFormData, TopicName } from 'types';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterTopicsPath } from 'lib/paths'; import { clusterTopicsPath } from 'lib/paths';
import { useForm, ErrorMessage } from 'react-hook-form'; import { useForm, ErrorMessage } from 'react-hook-form';
@ -11,16 +11,34 @@ import {
interface Props { interface Props {
clusterId: ClusterId; clusterId: ClusterId;
isTopicCreated: boolean;
createTopic: (clusterId: ClusterId, form: TopicFormData) => void;
redirectToTopicPath: (clusterId: ClusterId, topicName: TopicName) => void;
} }
const New: React.FC<Props> = ({ const New: React.FC<Props> = ({
clusterId, clusterId,
isTopicCreated,
createTopic,
redirectToTopicPath,
}) => { }) => {
const { register, handleSubmit, errors } = useForm<TopicFormData>(); // initialise the hook const { register, handleSubmit, errors, getValues } = useForm<TopicFormData>();
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
const onSubmit = (data: TopicFormData) => { React.useEffect(
console.log(data); () => {
}; if (isSubmitting && isTopicCreated) {
const { name } = getValues();
redirectToTopicPath(clusterId, name);
}
},
[isSubmitting, isTopicCreated, redirectToTopicPath, clusterId, getValues],
);
const onSubmit = async (data: TopicFormData) => {
setIsSubmitting(true);
createTopic(clusterId, data);
}
return ( return (
<div className="section"> <div className="section">
@ -37,7 +55,6 @@ const New: React.FC<Props> = ({
<div className="box"> <div className="box">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={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 * Topic Name *
@ -54,6 +71,7 @@ const New: React.FC<Props> = ({
})} })}
name="name" name="name"
autoComplete="off" autoComplete="off"
disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name="name" /> <ErrorMessage errors={errors} name="name" />
@ -71,6 +89,7 @@ const New: React.FC<Props> = ({
defaultValue="1" defaultValue="1"
ref={register({ required: 'Number of partitions is required.' })} ref={register({ required: 'Number of partitions is required.' })}
name="partitions" name="partitions"
disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name="partitions" /> <ErrorMessage errors={errors} name="partitions" />
@ -90,6 +109,7 @@ const New: React.FC<Props> = ({
defaultValue="1" defaultValue="1"
ref={register({ required: 'Replication Factor is required.' })} ref={register({ required: 'Replication Factor is required.' })}
name="replicationFactor" name="replicationFactor"
disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name="replicationFactor" /> <ErrorMessage errors={errors} name="replicationFactor" />
@ -107,6 +127,7 @@ const New: React.FC<Props> = ({
defaultValue="1" defaultValue="1"
ref={register({ required: 'Min In Sync Replicas is required.' })} ref={register({ required: 'Min In Sync Replicas is required.' })}
name="minInSyncReplicas" name="minInSyncReplicas"
disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name="minInSyncReplicas" /> <ErrorMessage errors={errors} name="minInSyncReplicas" />
@ -120,7 +141,12 @@ const New: React.FC<Props> = ({
Cleanup policy Cleanup policy
</label> </label>
<div className="select is-block"> <div className="select is-block">
<select defaultValue={CleanupPolicy.Delete} name="cleanupPolicy"> <select
defaultValue={CleanupPolicy.Delete}
name="cleanupPolicy"
ref={register}
disabled={isSubmitting}
>
<option value={CleanupPolicy.Delete}> <option value={CleanupPolicy.Delete}>
Delete Delete
</option> </option>
@ -140,6 +166,7 @@ const New: React.FC<Props> = ({
defaultValue={MILLISECONDS_IN_DAY * 7} defaultValue={MILLISECONDS_IN_DAY * 7}
name="retentionMs" name="retentionMs"
ref={register} ref={register}
disabled={isSubmitting}
> >
<option value={MILLISECONDS_IN_DAY / 2 }> <option value={MILLISECONDS_IN_DAY / 2 }>
12 hours 12 hours
@ -169,6 +196,7 @@ const New: React.FC<Props> = ({
defaultValue={-1} defaultValue={-1}
name="retentionBytes" name="retentionBytes"
ref={register} ref={register}
disabled={isSubmitting}
> >
<option value={-1}> <option value={-1}>
Not Set Not Set
@ -201,6 +229,7 @@ const New: React.FC<Props> = ({
defaultValue="1000012" defaultValue="1000012"
ref={register({ required: 'Maximum message size in bytes is required' })} ref={register({ required: 'Maximum message size in bytes is required' })}
name="maxMessageBytes" name="maxMessageBytes"
disabled={isSubmitting}
/> />
<p className="help is-danger"> <p className="help is-danger">
<ErrorMessage errors={errors} name="maxMessageBytes" /> <ErrorMessage errors={errors} name="maxMessageBytes" />
@ -208,7 +237,7 @@ const New: React.FC<Props> = ({
</div> </div>
</div> </div>
<input type="submit" className="button is-primary"/> <input type="submit" className="button is-primary" disabled={isSubmitting} />
</form> </form>
</div> </div>
</div> </div>

View file

@ -1,7 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState } from 'types'; import { RootState, ClusterId, TopicFormData, TopicName, Action } from 'types';
import New from './New'; import New from './New';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
import { createTopic } from 'redux/reducers/topics/thunks';
import { getTopicCreated } from 'redux/reducers/topics/selectors';
import { clusterTopicPath } from 'lib/paths';
import { ThunkDispatch } from 'redux-thunk';
interface RouteProps { interface RouteProps {
clusterId: string; clusterId: string;
@ -11,9 +15,19 @@ interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { clusterId } } }: OwnProps) => ({ const mapStateToProps = (state: RootState, { match: { params: { clusterId } } }: OwnProps) => ({
clusterId, clusterId,
isTopicCreated: getTopicCreated(state),
});
const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, undefined, Action>, { history }: OwnProps) => ({
createTopic: (clusterId: ClusterId, form: TopicFormData) => {
dispatch(createTopic(clusterId, form))
},
redirectToTopicPath: (clusterId: ClusterId, topicName: TopicName) => {
history.push(clusterTopicPath(clusterId, topicName));
}
}); });
export default withRouter( export default withRouter(
connect(mapStateToProps)(New) connect(mapStateToProps, mapDispatchToProps)(New)
); );

View file

@ -4,6 +4,7 @@ import {
ClusterId, ClusterId,
TopicDetails, TopicDetails,
TopicConfig, TopicConfig,
TopicFormData,
} from 'types'; } from 'types';
import { import {
BASE_URL, BASE_URL,
@ -21,3 +22,33 @@ export const getTopicDetails = (clusterId: ClusterId, topicName: TopicName): Pro
export const getTopics = (clusterId: ClusterId): Promise<Topic[]> => export const getTopics = (clusterId: ClusterId): Promise<Topic[]> =>
fetch(`${BASE_URL}/clusters/${clusterId}/topics`, { ...BASE_PARAMS }) fetch(`${BASE_URL}/clusters/${clusterId}/topics`, { ...BASE_PARAMS })
.then(res => res.json()); .then(res => res.json());
export const postTopic = (clusterId: ClusterId, form: TopicFormData): Promise<Response> => {
const {
name,
partitions,
replicationFactor,
cleanupPolicy,
retentionBytes,
retentionMs,
maxMessageBytes,
minInSyncReplicas,
} = form;
const body = JSON.stringify({
name,
partitions,
replicationFactor,
configs: {
'cleanup.policy': cleanupPolicy,
'retention.ms': retentionMs,
'retention.bytes': retentionBytes,
'max.message.bytes': maxMessageBytes,
'min.insync.replicas': minInSyncReplicas,
}
});
return fetch(`${BASE_URL}/clusters/${clusterId}/topics`, {
...BASE_PARAMS,
method: 'POST',
body,
});
}

View file

@ -10,6 +10,10 @@ enum ActionType {
GET_TOPIC_CONFIG__REQUEST = 'GET_TOPIC_CONFIG__REQUEST', GET_TOPIC_CONFIG__REQUEST = 'GET_TOPIC_CONFIG__REQUEST',
GET_TOPIC_CONFIG__SUCCESS = 'GET_TOPIC_CONFIG__SUCCESS', GET_TOPIC_CONFIG__SUCCESS = 'GET_TOPIC_CONFIG__SUCCESS',
GET_TOPIC_CONFIG__FAILURE = 'GET_TOPIC_CONFIG__FAILURE', GET_TOPIC_CONFIG__FAILURE = 'GET_TOPIC_CONFIG__FAILURE',
POST_TOPIC__REQUEST = 'POST_TOPIC__REQUEST',
POST_TOPIC__SUCCESS = 'POST_TOPIC__SUCCESS',
POST_TOPIC__FAILURE = 'POST_TOPIC__FAILURE',
} }
export default ActionType; export default ActionType;

View file

@ -19,3 +19,9 @@ export const fetchTopicConfigAction = createAsyncAction(
ActionType.GET_TOPIC_CONFIG__SUCCESS, ActionType.GET_TOPIC_CONFIG__SUCCESS,
ActionType.GET_TOPIC_CONFIG__FAILURE, ActionType.GET_TOPIC_CONFIG__FAILURE,
)<undefined, { topicName: TopicName, config: TopicConfig[] }, undefined>(); )<undefined, { topicName: TopicName, config: TopicConfig[] }, undefined>();
export const createTopicAction = createAsyncAction(
ActionType.POST_TOPIC__REQUEST,
ActionType.POST_TOPIC__SUCCESS,
ActionType.POST_TOPIC__FAILURE,
)<undefined, undefined, undefined>();

View file

@ -10,6 +10,7 @@ const getTopicMap = (state: RootState) => topicsState(state).byName;
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS'); const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
const getTopicDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS'); const getTopicDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS');
const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG'); const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
export const getIsTopicListFetched = createSelector( export const getIsTopicListFetched = createSelector(
getTopicListFetchingStatus, getTopicListFetchingStatus,
@ -26,6 +27,11 @@ export const getTopicConfigFetched = createSelector(
(status) => status === FetchStatus.fetched, (status) => status === FetchStatus.fetched,
); );
export const getTopicCreated = createSelector(
getTopicCreationStatus,
(status) => status === FetchStatus.fetched,
);
export const getTopicList = createSelector( export const getTopicList = createSelector(
getIsTopicListFetched, getIsTopicListFetched,
getAllNames, getAllNames,

View file

@ -2,13 +2,15 @@ import {
getTopics, getTopics,
getTopicDetails, getTopicDetails,
getTopicConfig, getTopicConfig,
postTopic,
} from 'lib/api'; } from 'lib/api';
import { import {
fetchTopicListAction, fetchTopicListAction,
fetchTopicDetailsAction, fetchTopicDetailsAction,
fetchTopicConfigAction, fetchTopicConfigAction,
createTopicAction,
} from './actions'; } from './actions';
import { PromiseThunk, ClusterId, TopicName } from 'types'; import { PromiseThunk, ClusterId, TopicName, TopicFormData } from 'types';
export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => { export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => {
dispatch(fetchTopicListAction.request()); dispatch(fetchTopicListAction.request());
@ -39,3 +41,14 @@ export const fetchTopicConfig = (clusterId: ClusterId, topicName: TopicName): Pr
dispatch(fetchTopicConfigAction.failure()); dispatch(fetchTopicConfigAction.failure());
} }
} }
export const createTopic = (clusterId: ClusterId, form: TopicFormData): PromiseThunk<void> => async (dispatch) => {
dispatch(createTopicAction.request());
try {
await postTopic(clusterId, form);
dispatch(createTopicAction.success());
} catch (e) {
dispatch(createTopicAction.failure());
}
}