[UI] Details
This commit is contained in:
parent
4d82638f49
commit
cc98afca0d
16 changed files with 155 additions and 52 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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> = ({
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,28 +49,29 @@ 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>
|
|
||||||
<th>Partition ID</th>
|
|
||||||
<th>Broker leader</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{partitions.map(({ partition, leader }) => (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{partition}</td>
|
<th>Partition ID</th>
|
||||||
<td>{leader}</td>
|
<th>Broker leader</th>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{partitions.map(({ partition, leader }) => (
|
||||||
|
<tr key={`partition-list-item-key-${partition}`}>
|
||||||
|
<td>{partition}</td>
|
||||||
|
<td>{leader}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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[],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue