浏览代码

[UI] Create Topic action

Oleg Shuralev 5 年之前
父节点
当前提交
82d81dd847

+ 37 - 8
frontend/src/components/Topics/New/New.tsx

@@ -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) => {
-    console.log(data);
-  };
+  React.useEffect(
+    () => {
+      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>

+ 16 - 2
frontend/src/components/Topics/New/NewContainer.ts

@@ -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)
 );
 );

+ 31 - 0
frontend/src/lib/api/topics.ts

@@ -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,
+  });
+}

+ 4 - 0
frontend/src/redux/reducers/topics/actionType.ts

@@ -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;

+ 6 - 0
frontend/src/redux/reducers/topics/actions.ts

@@ -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>();

+ 6 - 0
frontend/src/redux/reducers/topics/selectors.ts

@@ -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,

+ 14 - 1
frontend/src/redux/reducers/topics/thunks.ts

@@ -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());
+  }
+}