[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>
{!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>
);

View file

@ -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> = ({

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
/>
))}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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[],
}