[UI] Details

This commit is contained in:
Oleg Shuralev 2020-01-12 21:39:43 +03:00
parent 4d82638f49
commit cc98afca0d
No known key found for this signature in database
GPG key ID: 0459DF80E1A2FD1B
16 changed files with 155 additions and 52 deletions

View file

@ -31,7 +31,7 @@ const Nav: React.FC<Props> = ({
</p> </p>
{!isClusterListFetched && <div className="loader" />} {!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> </aside>
); );

View file

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

View file

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

View file

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

View file

@ -1,19 +1,70 @@
import React from 'react'; import React from 'react';
import { ClusterId, TopicName } from 'types'; import { ClusterId, TopicName, TopicConfig } from 'types';
interface Props { interface Props {
clusterId: ClusterId; clusterId: ClusterId;
topicName: TopicName; 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> = ({ const Sertings: React.FC<Props> = ({
clusterId, clusterId,
topicName, topicName,
isFetched,
fetchTopicConfig,
config,
}) => { }) => {
React.useEffect(
() => { fetchTopicConfig(clusterId, topicName); },
[fetchTopicConfig, clusterId, topicName],
);
if (!isFetched || !config) {
return (null);
}
return ( return (
<h1> <div className="box">
Settings {clusterId}/{topicName} <table className="table is-striped is-fullwidth">
</h1> <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>
); );
} }

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import cx from 'classnames'; import cx from 'classnames';
import { NavLink } from 'react-router-dom'; 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, name,
internal, internal,
partitions, partitions,

View file

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

View file

@ -3,14 +3,14 @@ import {
Topic, Topic,
ClusterId, ClusterId,
TopicDetails, TopicDetails,
TopicConfigs, TopicConfig,
} from 'types'; } from 'types';
import { import {
BASE_URL, BASE_URL,
BASE_PARAMS, BASE_PARAMS,
} from 'lib/constants'; } 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 }) fetch(`${BASE_URL}/clusters/${clusterId}/topics/${topicName}/config`, { ...BASE_PARAMS })
.then(res => res.json()); .then(res => res.json());

View file

@ -6,6 +6,10 @@ enum ActionType {
GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST', GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST',
GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS', GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS',
GET_TOPIC_DETAILS__FAILURE = 'GET_TOPIC_DETAILS__FAILURE', 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; export default ActionType;

View file

@ -1,6 +1,6 @@
import { createAsyncAction} from 'typesafe-actions'; import { createAsyncAction} from 'typesafe-actions';
import ActionType from './actionType'; import ActionType from './actionType';
import { Topic, TopicDetails, TopicName} from 'types'; import { Topic, TopicDetails, TopicName, TopicConfig} from 'types';
export const fetchTopicListAction = createAsyncAction( export const fetchTopicListAction = createAsyncAction(
ActionType.GET_TOPICS__REQUEST, ActionType.GET_TOPICS__REQUEST,
@ -13,3 +13,9 @@ export const fetchTopicDetailsAction = createAsyncAction(
ActionType.GET_TOPIC_DETAILS__SUCCESS, ActionType.GET_TOPIC_DETAILS__SUCCESS,
ActionType.GET_TOPIC_DETAILS__FAILURE, ActionType.GET_TOPIC_DETAILS__FAILURE,
)<undefined, { topicName: TopicName, details: TopicDetails }, undefined>(); )<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>();

View file

@ -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: default:
return state; return state;
} }

View file

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

View file

@ -1,10 +1,12 @@
import { import {
getTopics, getTopics,
getTopicDetails, getTopicDetails,
getTopicConfig,
} from 'lib/api'; } from 'lib/api';
import { import {
fetchTopicListAction, fetchTopicListAction,
fetchTopicDetailsAction, fetchTopicDetailsAction,
fetchTopicConfigAction,
} from './actions'; } from './actions';
import { PromiseThunk, ClusterId, TopicName } from 'types'; import { PromiseThunk, ClusterId, TopicName } from 'types';
@ -27,3 +29,13 @@ export const fetchTopicDetails = (clusterId: ClusterId, topicName: TopicName): P
dispatch(fetchTopicDetailsAction.failure()); 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());
}
}

View file

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