[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": {
|
"@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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 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">
|
||||||
<Breadcrumb links={[
|
<div className="level">
|
||||||
{ href: `/clusters/${clusterId}/topics`, label: 'All Topics' },
|
<div className="level-item level-left">
|
||||||
]}>
|
<Breadcrumb links={[
|
||||||
{name}
|
{ href: clusterTopicsPath(clusterId), label: 'All Topics' },
|
||||||
</Breadcrumb>
|
]}>
|
||||||
|
{topicName}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
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>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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)}>
|
||||||
<h5 className="subtitle is-6">
|
{title && (
|
||||||
{title}
|
<h5 className="subtitle is-6">
|
||||||
</h5>
|
{title}
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
<div className="level">
|
<div className="level">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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
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__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;
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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,22 +6,42 @@ export const initialState: TopicsState = {
|
||||||
allNames: [],
|
allNames: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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],
|
||||||
|
...topic,
|
||||||
|
};
|
||||||
|
memo.allNames.push(name);
|
||||||
|
|
||||||
|
return memo;
|
||||||
|
},
|
||||||
|
initialMemo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const reducer = (state = initialState, action: Action): TopicsState => {
|
const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actionType.GET_TOPICS__SUCCESS:
|
case actionType.GET_TOPICS__SUCCESS:
|
||||||
return action.payload.reduce(
|
return updateTopicList(state, action.payload);
|
||||||
(memo, topic) => {
|
case actionType.GET_TOPIC_DETAILS__SUCCESS:
|
||||||
const { name } = topic;
|
return {
|
||||||
memo.byName[name] = {
|
...state,
|
||||||
...memo.byName[name],
|
byName: {
|
||||||
...topic,
|
...state.byName,
|
||||||
};
|
[action.payload.topicName]: {
|
||||||
memo.allNames.push(name);
|
...state.byName[action.payload.topicName],
|
||||||
|
...action.payload.details,
|
||||||
return memo;
|
}
|
||||||
},
|
}
|
||||||
state,
|
}
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue