Jelajahi Sumber

[UI] Details

Oleg Shuralev 5 tahun lalu
induk
melakukan
cc98afca0d

+ 1 - 1
frontend/src/components/Nav/Nav.tsx

@@ -31,7 +31,7 @@ const Nav: React.FC<Props> = ({
     </p>
     {!isClusterListFetched && <div className="loader" />}
 
-    {isClusterListFetched && clusters.map((cluster) => <ClusterMenu {...cluster} key={cluster.id}/>)}
+    {isClusterListFetched && clusters.map((cluster, index) => <ClusterMenu {...cluster} key={`cluster-list-item-key-${index}`}/>)}
   </aside>
 );
 

+ 0 - 2
frontend/src/components/Topics/Details/Details.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import cx from 'classnames';
 import { ClusterId, Topic, TopicDetails, TopicName } from 'types';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { NavLink, Switch, Route } from 'react-router-dom';
@@ -11,7 +10,6 @@ import SettingsContainer from './Settings/SettingsContainer';
 interface Props extends Topic, TopicDetails {
   clusterId: ClusterId;
   topicName: TopicName;
-  fetchTopicDetails: (clusterId: ClusterId, topicName: TopicName) => void;
 }
 
 const Details: React.FC<Props> = ({

+ 1 - 6
frontend/src/components/Topics/Details/DetailsContainer.ts

@@ -1,10 +1,6 @@
 import { connect } from 'react-redux';
-import {
-  fetchTopicDetails,
-} from 'redux/reducers/topics/thunks';
 import Details from './Details';
-import { RootState, TopicName, ClusterId } from 'types';
-import { getTopicByName } from 'redux/reducers/topics/selectors';
+import { RootState } from 'types';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 
 interface RouteProps {
@@ -17,7 +13,6 @@ interface OwnProps extends RouteComponentProps<RouteProps> { }
 const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterId } } }: OwnProps) => ({
   clusterId,
   topicName,
-  ...getTopicByName(state, topicName),
 });
 
 export default withRouter(

+ 19 - 18
frontend/src/components/Topics/Details/Overview/Overview.tsx

@@ -34,7 +34,7 @@ const Overview: React.FC<Props> = ({
 
   return (
     <>
-      <MetricsWrapper wrapperClassName="notification">
+      <MetricsWrapper>
         <Indicator label="Partitions">
           {partitionCount}
         </Indicator>
@@ -49,28 +49,29 @@ const Overview: React.FC<Props> = ({
           <span className="subtitle has-text-weight-light"> of {replicas}</span>
         </Indicator>
         <Indicator label="Type">
-          <div className="tag is-primary">
+          <span className="tag is-primary">
             {internal ? 'Internal' : 'External'}
-          </div>
+          </span>
         </Indicator>
       </MetricsWrapper>
-
-      <table className="table is-striped is-fullwidth">
-        <thead>
-          <tr>
-            <th>Partition ID</th>
-            <th>Broker leader</th>
-          </tr>
-        </thead>
-        <tbody>
-          {partitions.map(({ partition, leader }) => (
+      <div className="box">
+        <table className="table is-striped is-fullwidth">
+          <thead>
             <tr>
-              <td>{partition}</td>
-              <td>{leader}</td>
+              <th>Partition ID</th>
+              <th>Broker leader</th>
             </tr>
-          ))}
-        </tbody>
-      </table>
+          </thead>
+          <tbody>
+            {partitions.map(({ partition, leader }) => (
+              <tr key={`partition-list-item-key-${partition}`}>
+                <td>{partition}</td>
+                <td>{leader}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
     </>
   );
 }

+ 55 - 4
frontend/src/components/Topics/Details/Settings/Settings.tsx

@@ -1,19 +1,70 @@
 import React from 'react';
-import { ClusterId, TopicName } from 'types';
+import { ClusterId, TopicName, TopicConfig } from 'types';
 
 interface Props {
   clusterId: ClusterId;
   topicName: TopicName;
+  config?: TopicConfig[];
+  isFetched: boolean;
+  fetchTopicConfig: (clusterId: ClusterId, topicName: TopicName) => void;
+}
+
+const ConfigListItem: React.FC<TopicConfig> = ({
+  name,
+  value,
+  defaultValue,
+}) => {
+  const hasCustomValue = value !== defaultValue;
+
+  return (
+    <tr>
+      <td className={hasCustomValue ? 'has-text-weight-bold' : ''}>
+        {name}
+      </td>
+      <td className={hasCustomValue ? 'has-text-weight-bold' : ''}>
+        {value}
+      </td>
+      <td
+        className="has-text-grey"
+        title="Default Value"
+      >
+        {hasCustomValue && defaultValue}
+      </td>
+    </tr>
+  )
 }
 
 const Sertings: React.FC<Props> = ({
   clusterId,
   topicName,
+  isFetched,
+  fetchTopicConfig,
+  config,
 }) => {
+  React.useEffect(
+    () => { fetchTopicConfig(clusterId, topicName); },
+    [fetchTopicConfig, clusterId, topicName],
+  );
+
+  if (!isFetched || !config) {
+    return (null);
+  }
+
   return (
-    <h1>
-      Settings {clusterId}/{topicName}
-    </h1>
+    <div className="box">
+      <table className="table is-striped is-fullwidth">
+        <thead>
+          <tr>
+            <th>Key</th>
+            <th>Value</th>
+            <th>Default Value</th>
+          </tr>
+        </thead>
+        <tbody>
+          {config.map((item, index) => <ConfigListItem key={`config-list-item-key-${index}`} {...item} />)}
+        </tbody>
+      </table>
+    </div>
   );
 }
 

+ 15 - 4
frontend/src/components/Topics/Details/Settings/SettingsContainer.ts

@@ -1,10 +1,15 @@
 import { connect } from 'react-redux';
+import { RootState, ClusterId, TopicName } from 'types';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
 import {
-  fetchTopicDetails,
+  fetchTopicConfig,
 } from 'redux/reducers/topics/thunks';
 import Settings from './Settings';
-import { RootState } from 'types';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import {
+  getTopicConfig,
+  getTopicConfigFetched,
+} from 'redux/reducers/topics/selectors';
+
 
 interface RouteProps {
   clusterId: string;
@@ -16,8 +21,14 @@ interface OwnProps extends RouteComponentProps<RouteProps> { }
 const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterId } } }: OwnProps) => ({
   clusterId,
   topicName,
+  config: getTopicConfig(state, topicName),
+  isFetched: getTopicConfigFetched(state),
 });
 
+const mapDispatchToProps = {
+  fetchTopicConfig: (clusterId: ClusterId, topicName: TopicName) => fetchTopicConfig(clusterId, topicName),
+}
+
 export default withRouter(
-  connect(mapStateToProps)(Settings)
+  connect(mapStateToProps, mapDispatchToProps)(Settings)
 );

+ 5 - 5
frontend/src/components/Topics/List/List.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
-import { Topic, TopicDetails } from 'types';
+import { TopicWithDetailedInfo } from 'types';
 import ListItem from './ListItem';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 
 interface Props {
-  topics: (Topic & TopicDetails)[];
-  externalTopics: (Topic & TopicDetails)[];
+  topics: (TopicWithDetailedInfo)[];
+  externalTopics: (TopicWithDetailedInfo)[];
 }
 
 const List: React.FC<Props> = ({
@@ -48,9 +48,9 @@ const List: React.FC<Props> = ({
             </tr>
           </thead>
           <tbody>
-            {items.map((topic) => (
+            {items.map((topic, index) => (
               <ListItem
-                key={topic.name}
+                key={`topic-list-item-key-${index}`}
                 {...topic}
               />
             ))}

+ 2 - 2
frontend/src/components/Topics/List/ListItem.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import cx from 'classnames';
 import { NavLink } from 'react-router-dom';
-import { Topic, TopicDetails } from 'types';
+import { TopicWithDetailedInfo } from 'types';
 
-const ListItem: React.FC<Topic & TopicDetails> = ({
+const ListItem: React.FC<TopicWithDetailedInfo> = ({
   name,
   internal,
   partitions,

+ 2 - 2
frontend/src/components/common/Breadcrumb/Breadcrumb.tsx

@@ -17,8 +17,8 @@ const Breadcrumb: React.FC<Props> = ({
   return (
     <nav className="breadcrumb" aria-label="breadcrumbs">
       <ul>
-        {links && links.map(({ label, href }) => (
-          <li key={label}>
+        {links && links.map(({ label, href }, index) => (
+          <li key={`breadcrumb-item-key-${index}`}>
             <NavLink to={href}>{label}</NavLink>
           </li>
         ))}

+ 2 - 2
frontend/src/lib/api/topics.ts

@@ -3,14 +3,14 @@ import {
   Topic,
   ClusterId,
   TopicDetails,
-  TopicConfigs,
+  TopicConfig,
 } from 'types';
 import {
   BASE_URL,
   BASE_PARAMS,
 } from 'lib/constants';
 
-export const getTopicConfig = (clusterId: ClusterId, topicName: TopicName): Promise<TopicConfigs> =>
+export const getTopicConfig = (clusterId: ClusterId, topicName: TopicName): Promise<TopicConfig[]> =>
   fetch(`${BASE_URL}/clusters/${clusterId}/topics/${topicName}/config`, { ...BASE_PARAMS })
     .then(res => res.json());
 

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

@@ -6,6 +6,10 @@ enum ActionType {
   GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST',
   GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS',
   GET_TOPIC_DETAILS__FAILURE = 'GET_TOPIC_DETAILS__FAILURE',
+
+  GET_TOPIC_CONFIG__REQUEST = 'GET_TOPIC_CONFIG__REQUEST',
+  GET_TOPIC_CONFIG__SUCCESS = 'GET_TOPIC_CONFIG__SUCCESS',
+  GET_TOPIC_CONFIG__FAILURE = 'GET_TOPIC_CONFIG__FAILURE',
 }
 
 export default ActionType;

+ 7 - 1
frontend/src/redux/reducers/topics/actions.ts

@@ -1,6 +1,6 @@
 import { createAsyncAction} from 'typesafe-actions';
 import ActionType from './actionType';
-import { Topic, TopicDetails, TopicName} from 'types';
+import { Topic, TopicDetails, TopicName, TopicConfig} from 'types';
 
 export const fetchTopicListAction = createAsyncAction(
   ActionType.GET_TOPICS__REQUEST,
@@ -13,3 +13,9 @@ export const fetchTopicDetailsAction = createAsyncAction(
   ActionType.GET_TOPIC_DETAILS__SUCCESS,
   ActionType.GET_TOPIC_DETAILS__FAILURE,
 )<undefined, { topicName: TopicName, details: TopicDetails }, undefined>();
+
+export const fetchTopicConfigAction = createAsyncAction(
+  ActionType.GET_TOPIC_CONFIG__REQUEST,
+  ActionType.GET_TOPIC_CONFIG__SUCCESS,
+  ActionType.GET_TOPIC_CONFIG__FAILURE,
+)<undefined, { topicName: TopicName, config: TopicConfig[] }, undefined>();

+ 11 - 0
frontend/src/redux/reducers/topics/reducer.ts

@@ -42,6 +42,17 @@ const reducer = (state = initialState, action: Action): TopicsState => {
           }
         }
       }
+    case actionType.GET_TOPIC_CONFIG__SUCCESS:
+      return {
+        ...state,
+        byName: {
+          ...state.byName,
+          [action.payload.topicName]: {
+            ...state.byName[action.payload.topicName],
+            config: action.payload.config,
+          }
+        }
+      }
     default:
       return state;
   }

+ 10 - 2
frontend/src/redux/reducers/topics/selectors.ts

@@ -8,7 +8,8 @@ const getAllNames = (state: RootState) => topicsState(state).allNames;
 const getTopicMap = (state: RootState) => topicsState(state).byName;
 
 const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
-const getTopiDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS');
+const getTopicDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS');
+const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 
 export const getIsTopicListFetched = createSelector(
   getTopicListFetchingStatus,
@@ -16,7 +17,12 @@ export const getIsTopicListFetched = createSelector(
 );
 
 export const getIsTopicDetailsFetched = createSelector(
-  getTopicListFetchingStatus,
+  getTopicDetailsFetchingStatus,
+  (status) => status === FetchStatus.fetched,
+);
+
+export const getTopicConfigFetched = createSelector(
+  getTopicConfigFetchingStatus,
   (status) => status === FetchStatus.fetched,
 );
 
@@ -45,3 +51,5 @@ export const getTopicByName = createSelector(
   getTopicName,
   (topics, topicName) => topics[topicName],
 );
+
+export const getTopicConfig = createSelector(getTopicByName, ({ config }) => config);

+ 12 - 0
frontend/src/redux/reducers/topics/thunks.ts

@@ -1,10 +1,12 @@
 import {
   getTopics,
   getTopicDetails,
+  getTopicConfig,
 } from 'lib/api';
 import {
   fetchTopicListAction,
   fetchTopicDetailsAction,
+  fetchTopicConfigAction,
 } from './actions';
 import { PromiseThunk, ClusterId, TopicName } from 'types';
 
@@ -27,3 +29,13 @@ export const fetchTopicDetails = (clusterId: ClusterId, topicName: TopicName): P
     dispatch(fetchTopicDetailsAction.failure());
   }
 }
+
+export const fetchTopicConfig = (clusterId: ClusterId, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
+  dispatch(fetchTopicConfigAction.request());
+  try {
+    const config = await getTopicConfig(clusterId, topicName);
+    dispatch(fetchTopicConfigAction.success({ topicName, config }));
+  } catch (e) {
+    dispatch(fetchTopicConfigAction.failure());
+  }
+}

+ 9 - 3
frontend/src/types/topic.ts

@@ -1,6 +1,8 @@
 export type TopicName = string;
-export interface TopicConfigs {
-  [key: string]: string;
+export interface TopicConfig {
+  name: string;
+  value: string;
+  defaultValue: string;
 }
 
 export interface TopicReplica {
@@ -31,7 +33,11 @@ export interface Topic {
   partitions: TopicPartition[];
 }
 
+export interface TopicWithDetailedInfo extends Topic, TopicDetails {
+  config?: TopicConfig[];
+}
+
 export interface TopicsState {
-  byName: { [topicName: string]: Topic & TopicDetails },
+  byName: { [topicName: string]: TopicWithDetailedInfo },
   allNames: TopicName[],
 }