[UI] Topic Details
This commit is contained in:
parent
ea4b9dc0b4
commit
ff852b390e
26 changed files with 360 additions and 144 deletions
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
|
@ -1489,9 +1489,9 @@
|
|||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "24.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.24.tgz",
|
||||
"integrity": "sha512-vgaG968EDPSJPMunEDdZvZgvxYSmeH8wKqBlHSkBt1pV2XlLEVDzsj1ZhLuI4iG4Pv841tES61txSBF0obh4CQ==",
|
||||
"version": "24.0.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.25.tgz",
|
||||
"integrity": "sha512-hnP1WpjN4KbGEK4dLayul6lgtys6FPz0UfxMeMQCv0M+sTnzN3ConfiO72jHgLxl119guHgI8gLqDOrRLsyp2g==",
|
||||
"requires": {
|
||||
"jest-diff": "^24.3.0"
|
||||
}
|
||||
|
@ -1507,9 +1507,9 @@
|
|||
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.12.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.21.tgz",
|
||||
"integrity": "sha512-8sRGhbpU+ck1n0PGAUgVrWrWdjSW2aqNeyC15W88GRsMpSwzv6RJGlLhE7s2RhVSOdyDmxbqlWSeThq4/7xqlA=="
|
||||
"version": "12.12.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz",
|
||||
"integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug=="
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -1527,9 +1527,9 @@
|
|||
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
|
||||
"integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
|
||||
"version": "16.9.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.17.tgz",
|
||||
"integrity": "sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
|
@ -11745,9 +11745,9 @@
|
|||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz",
|
||||
"integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
||||
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"symbol-observable": "^1.2.0"
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/jest": "^24.0.25",
|
||||
"@types/node": "^12.12.24",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.0",
|
||||
"redux": "^4.0.4",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"typesafe-actions": "^5.1.0",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { Cluster } from 'types';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
|
||||
|
||||
interface Props extends Cluster {}
|
||||
|
||||
|
@ -26,15 +27,15 @@ const ClusterMenu: React.FC<Props> = ({
|
|||
}) => (
|
||||
<ul className="menu-list">
|
||||
<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 />}
|
||||
{name}
|
||||
</NavLink>
|
||||
<ul>
|
||||
<NavLink to={`/clusters/${id}/brokers`} activeClassName="is-active" title="Brokers">
|
||||
<NavLink to={clusterBrokersPath(id)} activeClassName="is-active" title="Brokers">
|
||||
Brokers
|
||||
</NavLink>
|
||||
<NavLink to={`/clusters/${id}/topics`} activeClassName="is-active" title="Topics">
|
||||
<NavLink to={clusterTopicsPath(id)} activeClassName="is-active" title="Topics">
|
||||
Topics
|
||||
</NavLink>
|
||||
</ul>
|
||||
|
|
|
@ -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;
|
|
@ -1,35 +1,59 @@
|
|||
import React from 'react';
|
||||
import { ClusterId, Topic, TopicDetails } from 'types';
|
||||
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
||||
import Indicator from 'components/common/Dashboard/Indicator';
|
||||
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';
|
||||
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 {
|
||||
clusterId: ClusterId;
|
||||
topicName: TopicName;
|
||||
fetchTopicDetails: (clusterId: ClusterId, topicName: TopicName) => void;
|
||||
}
|
||||
|
||||
const Details: React.FC<Props> = ({
|
||||
clusterId,
|
||||
name,
|
||||
partitions,
|
||||
internal,
|
||||
topicName,
|
||||
}) => {
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
<div className="level-item level-left">
|
||||
<Breadcrumb links={[
|
||||
{ href: `/clusters/${clusterId}/topics`, label: 'All Topics' },
|
||||
{ href: clusterTopicsPath(clusterId), label: 'All Topics' },
|
||||
]}>
|
||||
{name}
|
||||
{topicName}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="level-item level-right">
|
||||
|
||||
<MetricsWrapper title="Partitions">
|
||||
<Indicator title="Under replicated partitions">
|
||||
0
|
||||
</Indicator>
|
||||
<Indicator title="Out of sync replicas">
|
||||
0
|
||||
</Indicator>
|
||||
</MetricsWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="box">
|
||||
<div className="tabs">
|
||||
<ul>
|
||||
<li className="is-active">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchTopicList,
|
||||
fetchTopicDetails,
|
||||
} from 'redux/reducers/topics/thunks';
|
||||
import Details from './Details';
|
||||
import { RootState } from 'types';
|
||||
import { RootState, TopicName, ClusterId } from 'types';
|
||||
import { getTopicByName } from 'redux/reducers/topics/selectors';
|
||||
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) => ({
|
||||
clusterId,
|
||||
topicName,
|
||||
...getTopicByName(state, topicName),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchTopicList,
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Details)
|
||||
connect(mapStateToProps)(Details)
|
||||
);
|
||||
|
|
20
frontend/src/components/Topics/Details/Messages/Messages.tsx
Normal file
20
frontend/src/components/Topics/Details/Messages/Messages.tsx
Normal 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;
|
|
@ -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)
|
||||
);
|
72
frontend/src/components/Topics/Details/Overview/Overview.tsx
Normal file
72
frontend/src/components/Topics/Details/Overview/Overview.tsx
Normal 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;
|
|
@ -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)
|
||||
);
|
|
@ -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;
|
|
@ -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;
|
20
frontend/src/components/Topics/Details/Settings/Settings.tsx
Normal file
20
frontend/src/components/Topics/Details/Settings/Settings.tsx
Normal 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;
|
|
@ -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)
|
||||
);
|
|
@ -44,6 +44,7 @@ const List: React.FC<Props> = ({
|
|||
<th>Topic Name</th>
|
||||
<th>Total Partitions</th>
|
||||
<th>Out of sync replicas</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Topic, TopicDetails } from 'types';
|
||||
|
||||
const ListItem: React.FC<Topic & TopicDetails> = ({
|
||||
name,
|
||||
internal,
|
||||
partitions,
|
||||
}) => {
|
||||
const outOfSyncReplicas = React.useMemo(() => {
|
||||
|
@ -26,6 +28,11 @@ const ListItem: React.FC<Topic & TopicDetails> = ({
|
|||
</td>
|
||||
<td>{partitions.length}</td>
|
||||
<td>{outOfSyncReplicas}</td>
|
||||
<td>
|
||||
<div className={cx('tag is-small', internal ? 'is-light' : 'is-success')}>
|
||||
{internal ? 'Internal' : 'External'}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,17 +18,15 @@ interface Props {
|
|||
const Topics: React.FC<Props> = ({
|
||||
clusterId,
|
||||
isFetched,
|
||||
fetchBrokers,
|
||||
fetchTopicList,
|
||||
}) => {
|
||||
React.useEffect(() => { fetchTopicList(clusterId); }, [fetchTopicList, clusterId]);
|
||||
// React.useEffect(() => { fetchBrokers(clusterId); }, [fetchBrokers, clusterId]);
|
||||
|
||||
if (isFetched) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
|
||||
<Route exact path="/clusters/:clusterId/topics" component={ListContainer} />
|
||||
<Route path="/clusters/:clusterId/topics/:topicName" component={DetailsContainer} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { fetchTopicList } from 'redux/reducers/topics/thunks';
|
||||
import { fetchBrokers } from 'redux/reducers/brokers/thunks';
|
||||
import Topics from './Topics';
|
||||
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
|
||||
import { RootState, ClusterId } from 'types';
|
||||
|
@ -19,7 +18,6 @@ const mapStateToProps = (state: RootState, { match: { params: { clusterId } }}:
|
|||
|
||||
const mapDispatchToProps = {
|
||||
fetchTopicList: (clusterId: ClusterId) => fetchTopicList(clusterId),
|
||||
fetchBrokers: (clusterId: ClusterId) => fetchBrokers(clusterId),
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
title?: string;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
const MetricsWrapper: React.FC<Props> = ({
|
||||
title,
|
||||
children,
|
||||
wrapperClassName,
|
||||
}) => {
|
||||
return (
|
||||
<div className="box">
|
||||
<div className={cx('box', wrapperClassName)}>
|
||||
{title && (
|
||||
<h5 className="subtitle is-6">
|
||||
{title}
|
||||
</h5>
|
||||
)}
|
||||
<div className="level">
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -2,14 +2,20 @@ import {
|
|||
TopicName,
|
||||
Topic,
|
||||
ClusterId,
|
||||
TopicDetails,
|
||||
TopicConfigs,
|
||||
} from 'types';
|
||||
import {
|
||||
BASE_URL,
|
||||
BASE_PARAMS,
|
||||
} from 'lib/constants';
|
||||
|
||||
export const getTopic = (name: TopicName): Promise<Topic> =>
|
||||
fetch(`${BASE_URL}/topics/${name}`, { ...BASE_PARAMS })
|
||||
export const getTopicConfig = (clusterId: ClusterId, topicName: TopicName): Promise<TopicConfigs> =>
|
||||
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());
|
||||
|
||||
export const getTopics = (clusterId: ClusterId): Promise<Topic[]> =>
|
||||
|
|
11
frontend/src/lib/paths.ts
Normal file
11
frontend/src/lib/paths.ts
Normal 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`;
|
|
@ -2,6 +2,10 @@ enum ActionType {
|
|||
GET_TOPICS__REQUEST = 'GET_TOPICS__REQUEST',
|
||||
GET_TOPICS__SUCCESS = 'GET_TOPICS__SUCCESS',
|
||||
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;
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { createAsyncAction} from 'typesafe-actions';
|
||||
import ActionType from './actionType';
|
||||
import { Topic} from 'types';
|
||||
import { Topic, TopicDetails, TopicName} from 'types';
|
||||
|
||||
export const fetchTopicListAction = createAsyncAction(
|
||||
ActionType.GET_TOPICS__REQUEST,
|
||||
ActionType.GET_TOPICS__SUCCESS,
|
||||
ActionType.GET_TOPICS__FAILURE,
|
||||
)<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>();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Action, TopicsState } from 'types';
|
||||
import { Action, TopicsState, Topic } from 'types';
|
||||
import actionType from 'redux/reducers/actionType';
|
||||
|
||||
export const initialState: TopicsState = {
|
||||
|
@ -6,11 +6,14 @@ export const initialState: TopicsState = {
|
|||
allNames: [],
|
||||
};
|
||||
|
||||
const reducer = (state = initialState, action: Action): TopicsState => {
|
||||
switch (action.type) {
|
||||
case actionType.GET_TOPICS__SUCCESS:
|
||||
return action.payload.reduce(
|
||||
(memo, topic) => {
|
||||
const updateTopicList = (state: TopicsState, payload: Topic[]) => {
|
||||
const initialMemo: TopicsState = {
|
||||
...state,
|
||||
allNames: [],
|
||||
}
|
||||
|
||||
return payload.reduce(
|
||||
(memo: TopicsState, topic) => {
|
||||
const { name } = topic;
|
||||
memo.byName[name] = {
|
||||
...memo.byName[name],
|
||||
|
@ -20,8 +23,25 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
|||
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -8,12 +8,18 @@ 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');
|
||||
|
||||
export const getIsTopicListFetched = createSelector(
|
||||
getTopicListFetchingStatus,
|
||||
(status) => status === FetchStatus.fetched,
|
||||
);
|
||||
|
||||
export const getIsTopicDetailsFetched = createSelector(
|
||||
getTopicListFetchingStatus,
|
||||
(status) => status === FetchStatus.fetched,
|
||||
);
|
||||
|
||||
export const getTopicList = createSelector(
|
||||
getIsTopicListFetched,
|
||||
getAllNames,
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { getTopics } from 'lib/api';
|
||||
import { fetchTopicListAction } from './actions';
|
||||
import { PromiseThunk, ClusterId } from 'types';
|
||||
import {
|
||||
getTopics,
|
||||
getTopicDetails,
|
||||
} from 'lib/api';
|
||||
import {
|
||||
fetchTopicListAction,
|
||||
fetchTopicDetailsAction,
|
||||
} from './actions';
|
||||
import { PromiseThunk, ClusterId, TopicName } from 'types';
|
||||
|
||||
export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => {
|
||||
dispatch(fetchTopicListAction.request());
|
||||
|
@ -11,3 +17,13 @@ export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => asyn
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue