[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>
|
||||
{!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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,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(
|
||||
|
|
|
@ -34,7 +34,7 @@ const Overview: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<MetricsWrapper wrapperClassName="notification">
|
||||
<MetricsWrapper>
|
||||
<Indicator label="Partitions">
|
||||
{partitionCount}
|
||||
</Indicator>
|
||||
|
@ -49,12 +49,12 @@ 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>
|
||||
|
||||
<div className="box">
|
||||
<table className="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -64,13 +64,14 @@ const Overview: React.FC<Props> = ({
|
|||
</thead>
|
||||
<tbody>
|
||||
{partitions.map(({ partition, leader }) => (
|
||||
<tr>
|
||||
<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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[],
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue