[UI] Topic Details

This commit is contained in:
Oleg Shuralev 2020-01-12 01:53:04 +03:00
parent ea4b9dc0b4
commit ff852b390e
No known key found for this signature in database
GPG key ID: 0459DF80E1A2FD1B
26 changed files with 360 additions and 144 deletions

View file

@ -1489,9 +1489,9 @@
} }
}, },
"@types/jest": { "@types/jest": {
"version": "24.0.24", "version": "24.0.25",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.24.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.25.tgz",
"integrity": "sha512-vgaG968EDPSJPMunEDdZvZgvxYSmeH8wKqBlHSkBt1pV2XlLEVDzsj1ZhLuI4iG4Pv841tES61txSBF0obh4CQ==", "integrity": "sha512-hnP1WpjN4KbGEK4dLayul6lgtys6FPz0UfxMeMQCv0M+sTnzN3ConfiO72jHgLxl119guHgI8gLqDOrRLsyp2g==",
"requires": { "requires": {
"jest-diff": "^24.3.0" "jest-diff": "^24.3.0"
} }
@ -1507,9 +1507,9 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
}, },
"@types/node": { "@types/node": {
"version": "12.12.21", "version": "12.12.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz",
"integrity": "sha512-8sRGhbpU+ck1n0PGAUgVrWrWdjSW2aqNeyC15W88GRsMpSwzv6RJGlLhE7s2RhVSOdyDmxbqlWSeThq4/7xqlA==" "integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug=="
}, },
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@ -1527,9 +1527,9 @@
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw=="
}, },
"@types/react": { "@types/react": {
"version": "16.9.16", "version": "16.9.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.17.tgz",
"integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==", "integrity": "sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^2.2.0" "csstype": "^2.2.0"
@ -11745,9 +11745,9 @@
} }
}, },
"redux": { "redux": {
"version": "4.0.4", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": { "requires": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0" "symbol-observable": "^1.2.0"

View file

@ -7,9 +7,9 @@
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"@types/classnames": "^2.2.9", "@types/classnames": "^2.2.9",
"@types/jest": "^24.0.0", "@types/jest": "^24.0.25",
"@types/node": "^12.0.0", "@types/node": "^12.12.24",
"@types/react": "^16.9.0", "@types/react": "^16.9.17",
"@types/react-dom": "^16.9.0", "@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.5", "@types/react-redux": "^7.1.5",
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.3",
@ -24,7 +24,7 @@
"react-redux": "^7.1.3", "react-redux": "^7.1.3",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.3.0", "react-scripts": "3.3.0",
"redux": "^4.0.4", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"typesafe-actions": "^5.1.0", "typesafe-actions": "^5.1.0",

View file

@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { Cluster } from 'types'; import { Cluster } from 'types';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
interface Props extends Cluster {} interface Props extends Cluster {}
@ -26,15 +27,15 @@ const ClusterMenu: React.FC<Props> = ({
}) => ( }) => (
<ul className="menu-list"> <ul className="menu-list">
<li> <li>
<NavLink exact to={`/clusters/${id}/brokers`} title={name} className="has-text-overflow-ellipsis"> <NavLink exact to={clusterBrokersPath(id)} title={name} className="has-text-overflow-ellipsis">
{defaultCluster && <DefaultIcon />} {defaultCluster && <DefaultIcon />}
{name} {name}
</NavLink> </NavLink>
<ul> <ul>
<NavLink to={`/clusters/${id}/brokers`} activeClassName="is-active" title="Brokers"> <NavLink to={clusterBrokersPath(id)} activeClassName="is-active" title="Brokers">
Brokers Brokers
</NavLink> </NavLink>
<NavLink to={`/clusters/${id}/topics`} activeClassName="is-active" title="Topics"> <NavLink to={clusterTopicsPath(id)} activeClassName="is-active" title="Topics">
Topics Topics
</NavLink> </NavLink>
</ul> </ul>

View file

@ -1,13 +0,0 @@
import React from 'react';
const ConfigRow: React.FC<{name: string, value: string}> = ({
name,
value,
}) => (
<tr>
<td>{name}</td>
<td>{value}</td>
</tr>
);
export default ConfigRow;

View file

@ -1,35 +1,59 @@
import React from 'react'; import React from 'react';
import { ClusterId, Topic, TopicDetails } from 'types'; import cx from 'classnames';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; import { ClusterId, Topic, TopicDetails, TopicName } from 'types';
import Indicator from 'components/common/Dashboard/Indicator';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { NavLink, Switch, Route } from 'react-router-dom';
import { clusterTopicsPath, clusterTopicSettingsPath, clusterTopicPath, clusterTopicMessagesPath } from 'lib/paths';
import OverviewContainer from './Overview/OverviewContainer';
import MessagesContainer from './Messages/MessagesContainer';
import SettingsContainer from './Settings/SettingsContainer';
interface Props extends Topic, TopicDetails { interface Props extends Topic, TopicDetails {
clusterId: ClusterId; clusterId: ClusterId;
topicName: TopicName;
fetchTopicDetails: (clusterId: ClusterId, topicName: TopicName) => void;
} }
const Details: React.FC<Props> = ({ const Details: React.FC<Props> = ({
clusterId, clusterId,
name, topicName,
partitions,
internal,
}) => { }) => {
return ( return (
<div className="section"> <div className="section">
<div className="level">
<div className="level-item level-left">
<Breadcrumb links={[ <Breadcrumb links={[
{ href: `/clusters/${clusterId}/topics`, label: 'All Topics' }, { href: clusterTopicsPath(clusterId), label: 'All Topics' },
]}> ]}>
{name} {topicName}
</Breadcrumb> </Breadcrumb>
</div>
<div className="level-item level-right">
<MetricsWrapper title="Partitions"> </div>
<Indicator title="Under replicated partitions"> </div>
0
</Indicator> <div className="box">
<Indicator title="Out of sync replicas"> <div className="tabs">
0 <ul>
</Indicator> <li className="is-active">
</MetricsWrapper> <NavLink exact to={clusterTopicPath(clusterId, topicName)}>Overview</NavLink>
</li>
<li>
<NavLink exact to={clusterTopicMessagesPath(clusterId, topicName)}>Messages</NavLink>
</li>
<li>
<NavLink exact to={clusterTopicSettingsPath(clusterId, topicName)}>Settings</NavLink>
</li>
</ul>
</div>
<Switch>
<Route exact path="/clusters/:clusterId/topics/:topicName/messages" component={MessagesContainer} />
<Route exact path="/clusters/:clusterId/topics/:topicName/settings" component={SettingsContainer} />
<Route exact path="/clusters/:clusterId/topics/:topicName" component={OverviewContainer} />
</Switch>
</div>
</div> </div>
); );
} }

View file

@ -1,9 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
fetchTopicList, fetchTopicDetails,
} from 'redux/reducers/topics/thunks'; } from 'redux/reducers/topics/thunks';
import Details from './Details'; import Details from './Details';
import { RootState } from 'types'; import { RootState, TopicName, ClusterId } from 'types';
import { getTopicByName } from 'redux/reducers/topics/selectors'; import { getTopicByName } from 'redux/reducers/topics/selectors';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
@ -16,13 +16,10 @@ 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,
...getTopicByName(state, topicName), ...getTopicByName(state, topicName),
}); });
const mapDispatchToProps = {
fetchTopicList,
}
export default withRouter( export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Details) connect(mapStateToProps)(Details)
); );

View file

@ -0,0 +1,20 @@
import React from 'react';
import { ClusterId, TopicName } from 'types';
interface Props {
clusterId: ClusterId;
topicName: TopicName;
}
const Messages: React.FC<Props> = ({
clusterId,
topicName,
}) => {
return (
<h1>
Messages from {clusterId}{topicName}
</h1>
);
}
export default Messages;

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import Messages from './Messages';
import { RootState } from 'types';
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface RouteProps {
clusterId: string;
topicName: string;
}
interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterId } } }: OwnProps) => ({
clusterId,
topicName,
});
export default withRouter(
connect(mapStateToProps)(Messages)
);

View file

@ -0,0 +1,72 @@
import React from 'react';
import { ClusterId, Topic, TopicDetails, TopicName } from 'types';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator';
interface Props extends Topic, TopicDetails {
isFetched: boolean;
clusterId: ClusterId;
topicName: TopicName;
fetchTopicDetails: (clusterId: ClusterId, topicName: TopicName) => void;
}
const Overview: React.FC<Props> = ({
isFetched,
clusterId,
topicName,
partitions,
underReplicatedPartitions,
inSyncReplicas,
replicas,
partitionCount,
replicationFactor,
fetchTopicDetails,
}) => {
React.useEffect(
() => { fetchTopicDetails(clusterId, topicName); },
[fetchTopicDetails, clusterId, topicName],
);
if (!isFetched) {
return null;
}
return (
<>
<MetricsWrapper wrapperClassName="notification">
<Indicator title="Partitions">
{partitionCount}
</Indicator>
<Indicator title="Replication Factor">
{replicationFactor}
</Indicator>
<Indicator title="Under replicated partitions">
{underReplicatedPartitions}
</Indicator>
<Indicator title="In sync replicas">
{inSyncReplicas}
<span className="subtitle has-text-weight-light"> of {replicas}</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 }) => (
<tr>
<td>{partition}</td>
<td>{leader}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
export default Overview;

View file

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import {
fetchTopicDetails,
} from 'redux/reducers/topics/thunks';
import Overview from './Overview';
import { RootState, TopicName, ClusterId } from 'types';
import { getTopicByName, getIsTopicDetailsFetched } from 'redux/reducers/topics/selectors';
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface RouteProps {
clusterId: string;
topicName: string;
}
interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterId } } }: OwnProps) => ({
clusterId,
topicName,
isFetched: getIsTopicDetailsFetched(state),
...getTopicByName(state, topicName),
});
const mapDispatchToProps = {
fetchTopicDetails: (clusterId: ClusterId, topicName: TopicName) => fetchTopicDetails(clusterId, topicName),
}
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Overview)
);

View file

@ -1,22 +0,0 @@
import React from 'react';
import { TopicPartition } from 'types';
import Replica from './Replica';
const Partition: React.FC<TopicPartition> = ({
partition,
leader,
replicas,
}) => {
return (
<div className="tile is-child box">
<h2 className="title is-5">Partition #{partition}</h2>
<div className="columns is-mobile is-multiline">
{replicas.map((replica, index) => <Replica {...replica} index={index} />)}
</div>
</div>
);
};
export default Partition;

View file

@ -1,34 +0,0 @@
import React from 'react';
import { TopicReplica } from 'types';
import cx from 'classnames';
interface Props extends TopicReplica {
index: number;
}
const Replica: React.FC<Props> = ({
inSync,
leader,
broker,
index,
}) => {
return (
<div className="column is-narrow">
<div className={cx('notification', leader ? 'is-warning' : 'is-light')}>
<div className="title is-6">Replica #{index}</div>
<div className="tags">
{leader && (
<span className="tag">
LEADER
</span>
)}
<span className={cx('tag', inSync ? 'is-success' : 'is-danger')}>
{inSync ? 'IN SYNC' : 'OUT OF SYNC'}
</span>
</div>
</div>
</div>
);
};
export default Replica;

View file

@ -0,0 +1,20 @@
import React from 'react';
import { ClusterId, TopicName } from 'types';
interface Props {
clusterId: ClusterId;
topicName: TopicName;
}
const Sertings: React.FC<Props> = ({
clusterId,
topicName,
}) => {
return (
<h1>
Settings {clusterId}/{topicName}
</h1>
);
}
export default Sertings;

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import {
fetchTopicDetails,
} from 'redux/reducers/topics/thunks';
import Settings from './Settings';
import { RootState } from 'types';
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface RouteProps {
clusterId: string;
topicName: string;
}
interface OwnProps extends RouteComponentProps<RouteProps> { }
const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterId } } }: OwnProps) => ({
clusterId,
topicName,
});
export default withRouter(
connect(mapStateToProps)(Settings)
);

View file

@ -44,6 +44,7 @@ const List: React.FC<Props> = ({
<th>Topic Name</th> <th>Topic Name</th>
<th>Total Partitions</th> <th>Total Partitions</th>
<th>Out of sync replicas</th> <th>Out of sync replicas</th>
<th>Type</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import cx from 'classnames';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { Topic, TopicDetails } from 'types'; import { Topic, TopicDetails } from 'types';
const ListItem: React.FC<Topic & TopicDetails> = ({ const ListItem: React.FC<Topic & TopicDetails> = ({
name, name,
internal,
partitions, partitions,
}) => { }) => {
const outOfSyncReplicas = React.useMemo(() => { const outOfSyncReplicas = React.useMemo(() => {
@ -26,6 +28,11 @@ const ListItem: React.FC<Topic & TopicDetails> = ({
</td> </td>
<td>{partitions.length}</td> <td>{partitions.length}</td>
<td>{outOfSyncReplicas}</td> <td>{outOfSyncReplicas}</td>
<td>
<div className={cx('tag is-small', internal ? 'is-light' : 'is-success')}>
{internal ? 'Internal' : 'External'}
</div>
</td>
</tr> </tr>
); );
} }

View file

@ -18,17 +18,15 @@ interface Props {
const Topics: React.FC<Props> = ({ const Topics: React.FC<Props> = ({
clusterId, clusterId,
isFetched, isFetched,
fetchBrokers,
fetchTopicList, fetchTopicList,
}) => { }) => {
React.useEffect(() => { fetchTopicList(clusterId); }, [fetchTopicList, clusterId]); React.useEffect(() => { fetchTopicList(clusterId); }, [fetchTopicList, clusterId]);
// React.useEffect(() => { fetchBrokers(clusterId); }, [fetchBrokers, clusterId]);
if (isFetched) { if (isFetched) {
return ( return (
<Switch> <Switch>
<Route exact path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
<Route exact path="/clusters/:clusterId/topics" component={ListContainer} /> <Route exact path="/clusters/:clusterId/topics" component={ListContainer} />
<Route path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
</Switch> </Switch>
); );
} }

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTopicList } from 'redux/reducers/topics/thunks'; import { fetchTopicList } from 'redux/reducers/topics/thunks';
import { fetchBrokers } from 'redux/reducers/brokers/thunks';
import Topics from './Topics'; import Topics from './Topics';
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors'; import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
import { RootState, ClusterId } from 'types'; import { RootState, ClusterId } from 'types';
@ -19,7 +18,6 @@ const mapStateToProps = (state: RootState, { match: { params: { clusterId } }}:
const mapDispatchToProps = { const mapDispatchToProps = {
fetchTopicList: (clusterId: ClusterId) => fetchTopicList(clusterId), fetchTopicList: (clusterId: ClusterId) => fetchTopicList(clusterId),
fetchBrokers: (clusterId: ClusterId) => fetchBrokers(clusterId),
} }
export default connect(mapStateToProps, mapDispatchToProps)(Topics); export default connect(mapStateToProps, mapDispatchToProps)(Topics);

View file

@ -1,18 +1,23 @@
import React from 'react'; import React from 'react';
import cx from 'classnames';
interface Props { interface Props {
title: string; title?: string;
wrapperClassName?: string;
} }
const MetricsWrapper: React.FC<Props> = ({ const MetricsWrapper: React.FC<Props> = ({
title, title,
children, children,
wrapperClassName,
}) => { }) => {
return ( return (
<div className="box"> <div className={cx('box', wrapperClassName)}>
{title && (
<h5 className="subtitle is-6"> <h5 className="subtitle is-6">
{title} {title}
</h5> </h5>
)}
<div className="level"> <div className="level">
{children} {children}
</div> </div>

View file

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

11
frontend/src/lib/paths.ts Normal file
View file

@ -0,0 +1,11 @@
import { ClusterId, TopicName } from "types";
const clusterPath = (clusterId: ClusterId) => `/clusters/${clusterId}`;
export const clusterBrokersPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/brokers`;
export const clusterTopicsPath = (clusterId: ClusterId) => `${clusterPath(clusterId)}/topics`;
export const clusterTopicPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}`;
export const clusterTopicSettingsPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}/settings`;
export const clusterTopicMessagesPath = (clusterId: ClusterId, topicName: TopicName) => `${clusterTopicsPath(clusterId)}/${topicName}/messages`;

View file

@ -2,6 +2,10 @@ enum ActionType {
GET_TOPICS__REQUEST = 'GET_TOPICS__REQUEST', GET_TOPICS__REQUEST = 'GET_TOPICS__REQUEST',
GET_TOPICS__SUCCESS = 'GET_TOPICS__SUCCESS', GET_TOPICS__SUCCESS = 'GET_TOPICS__SUCCESS',
GET_TOPICS__FAILURE = 'GET_TOPICS__FAILURE', GET_TOPICS__FAILURE = 'GET_TOPICS__FAILURE',
GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST',
GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS',
GET_TOPIC_DETAILS__FAILURE = 'GET_TOPIC_DETAILS__FAILURE',
} }
export default ActionType; export default ActionType;

View file

@ -1,9 +1,15 @@
import { createAsyncAction} from 'typesafe-actions'; import { createAsyncAction} from 'typesafe-actions';
import ActionType from './actionType'; import ActionType from './actionType';
import { Topic} from 'types'; import { Topic, TopicDetails, TopicName} from 'types';
export const fetchTopicListAction = createAsyncAction( export const fetchTopicListAction = createAsyncAction(
ActionType.GET_TOPICS__REQUEST, ActionType.GET_TOPICS__REQUEST,
ActionType.GET_TOPICS__SUCCESS, ActionType.GET_TOPICS__SUCCESS,
ActionType.GET_TOPICS__FAILURE, ActionType.GET_TOPICS__FAILURE,
)<undefined, Topic[], undefined>(); )<undefined, Topic[], undefined>();
export const fetchTopicDetailsAction = createAsyncAction(
ActionType.GET_TOPIC_DETAILS__REQUEST,
ActionType.GET_TOPIC_DETAILS__SUCCESS,
ActionType.GET_TOPIC_DETAILS__FAILURE,
)<undefined, { topicName: TopicName, details: TopicDetails }, undefined>();

View file

@ -1,4 +1,4 @@
import { Action, TopicsState } from 'types'; import { Action, TopicsState, Topic } from 'types';
import actionType from 'redux/reducers/actionType'; import actionType from 'redux/reducers/actionType';
export const initialState: TopicsState = { export const initialState: TopicsState = {
@ -6,11 +6,14 @@ export const initialState: TopicsState = {
allNames: [], allNames: [],
}; };
const reducer = (state = initialState, action: Action): TopicsState => { const updateTopicList = (state: TopicsState, payload: Topic[]) => {
switch (action.type) { const initialMemo: TopicsState = {
case actionType.GET_TOPICS__SUCCESS: ...state,
return action.payload.reduce( allNames: [],
(memo, topic) => { }
return payload.reduce(
(memo: TopicsState, topic) => {
const { name } = topic; const { name } = topic;
memo.byName[name] = { memo.byName[name] = {
...memo.byName[name], ...memo.byName[name],
@ -20,8 +23,25 @@ const reducer = (state = initialState, action: Action): TopicsState => {
return memo; return memo;
}, },
state, initialMemo,
); );
}
const reducer = (state = initialState, action: Action): TopicsState => {
switch (action.type) {
case actionType.GET_TOPICS__SUCCESS:
return updateTopicList(state, action.payload);
case actionType.GET_TOPIC_DETAILS__SUCCESS:
return {
...state,
byName: {
...state.byName,
[action.payload.topicName]: {
...state.byName[action.payload.topicName],
...action.payload.details,
}
}
}
default: default:
return state; return state;
} }

View file

@ -8,12 +8,18 @@ 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');
export const getIsTopicListFetched = createSelector( export const getIsTopicListFetched = createSelector(
getTopicListFetchingStatus, getTopicListFetchingStatus,
(status) => status === FetchStatus.fetched, (status) => status === FetchStatus.fetched,
); );
export const getIsTopicDetailsFetched = createSelector(
getTopicListFetchingStatus,
(status) => status === FetchStatus.fetched,
);
export const getTopicList = createSelector( export const getTopicList = createSelector(
getIsTopicListFetched, getIsTopicListFetched,
getAllNames, getAllNames,

View file

@ -1,6 +1,12 @@
import { getTopics } from 'lib/api'; import {
import { fetchTopicListAction } from './actions'; getTopics,
import { PromiseThunk, ClusterId } from 'types'; getTopicDetails,
} from 'lib/api';
import {
fetchTopicListAction,
fetchTopicDetailsAction,
} from './actions';
import { PromiseThunk, ClusterId, TopicName } from 'types';
export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => { export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => {
dispatch(fetchTopicListAction.request()); dispatch(fetchTopicListAction.request());
@ -11,3 +17,13 @@ export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => asyn
dispatch(fetchTopicListAction.failure()); dispatch(fetchTopicListAction.failure());
} }
} }
export const fetchTopicDetails = (clusterId: ClusterId, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
dispatch(fetchTopicDetailsAction.request());
try {
const topicDetails = await getTopicDetails(clusterId, topicName);
dispatch(fetchTopicDetailsAction.success({ topicName, details: topicDetails }));
} catch (e) {
dispatch(fetchTopicDetailsAction.failure());
}
}