From e24423e61304aec0c61612b462a44a776cc7a50a Mon Sep 17 00:00:00 2001 From: Oleg Shuralev Date: Fri, 17 Jan 2020 22:35:19 +0300 Subject: [PATCH] [UI] Topic Creation Page --- frontend/src/components/Topics/List/List.tsx | 42 ++-- .../components/Topics/List/ListContainer.ts | 15 +- frontend/src/components/Topics/New/New.tsx | 218 ++++++++++++++++++ .../src/components/Topics/New/NewContainer.ts | 19 ++ frontend/src/components/Topics/Topics.tsx | 4 +- frontend/src/lib/constants.ts | 4 + frontend/src/lib/paths.ts | 1 + frontend/src/theme/bulma_overrides.scss | 4 + frontend/src/types/topic.ts | 17 ++ 9 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/Topics/New/New.tsx create mode 100644 frontend/src/components/Topics/New/NewContainer.ts diff --git a/frontend/src/components/Topics/List/List.tsx b/frontend/src/components/Topics/List/List.tsx index 9b3b0ac5a7..d193b077f4 100644 --- a/frontend/src/components/Topics/List/List.tsx +++ b/frontend/src/components/Topics/List/List.tsx @@ -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 = ({ + clusterId, topics, externalTopics, }) => { @@ -23,18 +27,30 @@ const List: React.FC = ({ All Topics
-
- - +
+
+
+ + +
+
+
+ + Add a Topic + +
diff --git a/frontend/src/components/Topics/List/ListContainer.ts b/frontend/src/components/Topics/List/ListContainer.ts index 81be3a36bd..6efbef3a59 100644 --- a/frontend/src/components/Topics/List/ListContainer.ts +++ b/frontend/src/components/Topics/List/ListContainer.ts @@ -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 { } + +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) +); diff --git a/frontend/src/components/Topics/New/New.tsx b/frontend/src/components/Topics/New/New.tsx new file mode 100644 index 0000000000..c89bd8f031 --- /dev/null +++ b/frontend/src/components/Topics/New/New.tsx @@ -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 = ({ + clusterId, +}) => { + const { register, handleSubmit, errors } = useForm(); // initialise the hook + + const onSubmit = (data: TopicFormData) => { + console.log(data); + }; + + return ( +
+
+
+ + New Topic + +
+
+ +
+
+
+ +
+ + +

+ +

+
+ +
+ + +

+ +

+
+
+ +
+
+ + +

+ +

+
+ +
+ + +

+ +

+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ + +

+ +

+
+
+ + +
+
+
+ ); +} + +export default New; diff --git a/frontend/src/components/Topics/New/NewContainer.ts b/frontend/src/components/Topics/New/NewContainer.ts new file mode 100644 index 0000000000..9dadbc0ee3 --- /dev/null +++ b/frontend/src/components/Topics/New/NewContainer.ts @@ -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 { } + +const mapStateToProps = (state: RootState, { match: { params: { clusterId } } }: OwnProps) => ({ + clusterId, +}); + + +export default withRouter( + connect(mapStateToProps)(New) +); diff --git a/frontend/src/components/Topics/Topics.tsx b/frontend/src/components/Topics/Topics.tsx index e67c14b22f..50d4c48b60 100644 --- a/frontend/src/components/Topics/Topics.tsx +++ b/frontend/src/components/Topics/Topics.tsx @@ -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 = ({ return ( + ); diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index 56e9cfd1f8..e2812085eb 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -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; diff --git a/frontend/src/lib/paths.ts b/frontend/src/lib/paths.ts index 26106947b8..787e5de7a2 100644 --- a/frontend/src/lib/paths.ts +++ b/frontend/src/lib/paths.ts @@ -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`; diff --git a/frontend/src/theme/bulma_overrides.scss b/frontend/src/theme/bulma_overrides.scss index 5bd74bb8d4..db34f13ba5 100644 --- a/frontend/src/theme/bulma_overrides.scss +++ b/frontend/src/theme/bulma_overrides.scss @@ -28,6 +28,10 @@ animation: fadein .5s; } +.select.is-block select { + width: 100%; +} + @keyframes fadein { from { opacity: 0; } to { opacity: 1; } diff --git a/frontend/src/types/topic.ts b/frontend/src/types/topic.ts index 875fb0aeaa..47c381b2d7 100644 --- a/frontend/src/types/topic.ts +++ b/frontend/src/types/topic.ts @@ -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; +};