[UI] Dashboard

This commit is contained in:
Oleg Shuralev 2020-01-19 20:50:59 +03:00
parent 870971f879
commit fad704f377
No known key found for this signature in database
GPG key ID: 0459DF80E1A2FD1B
7 changed files with 223 additions and 10 deletions

View file

@ -9,6 +9,7 @@ import BrokersContainer from './Brokers/BrokersContainer';
import TopicsContainer from './Topics/TopicsContainer'; import TopicsContainer from './Topics/TopicsContainer';
import NavConatiner from './Nav/NavConatiner'; import NavConatiner from './Nav/NavConatiner';
import PageLoader from './common/PageLoader/PageLoader'; import PageLoader from './common/PageLoader/PageLoader';
import Dashboard from './Dashboard/Dashboard';
interface AppProps { interface AppProps {
isClusterListFetched: boolean; isClusterListFetched: boolean;
@ -34,10 +35,7 @@ const App: React.FC<AppProps> = ({
<NavConatiner className="Layout__navbar" /> <NavConatiner className="Layout__navbar" />
{isClusterListFetched ? ( {isClusterListFetched ? (
<Switch> <Switch>
<Route exact path="/"> <Route exact path="/" component={Dashboard} />
Dashboard
</Route>
<Route path="/clusters/:clusterId/topics" component={TopicsContainer} /> <Route path="/clusters/:clusterId/topics" component={TopicsContainer} />
<Route path="/clusters/:clusterId/brokers" component={BrokersContainer} /> <Route path="/clusters/:clusterId/brokers" component={BrokersContainer} />
<Redirect from="/clusters/:clusterId" to="/clusters/:clusterId/brokers" /> <Redirect from="/clusters/:clusterId" to="/clusters/:clusterId/brokers" />

View file

@ -0,0 +1,59 @@
import React from 'react';
import { Cluster, ClusterStatus } from 'lib/interfaces';
import formatBytes from 'lib/utils/formatBytes';
import { NavLink } from 'react-router-dom';
import { clusterBrokersPath } from 'lib/paths';
const ClusterWidget: React.FC<Cluster> = ({
id,
name,
status,
topicCount,
brokerCount,
bytesInPerSec,
bytesOutPerSec,
onlinePartitionCount,
}) => (
<NavLink to={clusterBrokersPath(id)} className="column is-full-modile is-6">
<div className="box is-hoverable">
<div
className="title is-6 has-text-overflow-ellipsis"
title={name}
>
<div
className={`tag has-margin-right ${status === ClusterStatus.Online ? 'is-primary' : 'is-danger'}`}
>
{status}
</div>
{name}
</div>
<table className="table is-fullwidth">
<tbody>
<tr>
<th>Brokers</th>
<td>{brokerCount}</td>
</tr>
<tr>
<th>Partitions</th>
<td>{onlinePartitionCount}</td>
</tr>
<tr>
<th>Topics</th>
<td>{topicCount}</td>
</tr>
<tr>
<th>Production</th>
<td>{formatBytes(bytesInPerSec)}</td>
</tr>
<tr>
<th>Consumption</th>
<td>{formatBytes(bytesOutPerSec)}</td>
</tr>
</tbody>
</table>
</div>
</NavLink>
);
export default ClusterWidget;

View file

@ -0,0 +1,77 @@
import React from 'react';
import { chunk } from 'lodash';
import { Cluster } from 'lib/interfaces';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator';
import ClusterWidget from './ClusterWidget';
interface Props {
clusters: Cluster[];
onlineClusters: Cluster[];
offlineClusters: Cluster[];
}
const ClustersWidget: React.FC<Props> = ({
clusters,
onlineClusters,
offlineClusters,
}) => {
const [showOfflineOnly, setShowOfflineOnly] = React.useState<boolean>(false);
const clusterList: Array<Cluster[]> = React.useMemo(() => {
let list = clusters;
if (showOfflineOnly) {
list = offlineClusters;
}
return chunk(list, 2);
},
[clusters, offlineClusters, showOfflineOnly],
);
const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly);
return (
<div>
<h5 className="title is-5">
Clusters
</h5>
<MetricsWrapper>
<Indicator label="Online Clusters" >
<span className="tag is-primary">
{onlineClusters.length}
</span>
</Indicator>
<Indicator label="Offline Clusters">
<span className="tag is-danger">
{offlineClusters.length}
</span>
</Indicator>
<Indicator label="Hide online clusters">
<input
type="checkbox"
className="switch is-rounded"
name="switchRoundedDefault"
id="switchRoundedDefault"
checked={showOfflineOnly}
onChange={handleSwitch}
/>
<label htmlFor="switchRoundedDefault">
</label>
</Indicator>
</MetricsWrapper>
{clusterList.map((chunk, idx) => (
<div className="columns" key={`dashboard-cluster-list-row-key-${idx}`}>
{chunk.map((cluster, idx) => (
<ClusterWidget {...cluster} key={`dashboard-cluster-list-item-key-${idx}`}/>
))}
</div>
))}
</div>
)
};
export default ClustersWidget;

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import ClustersWidget from './ClustersWidget';
import {
getClusterList,
getOnlineClusters,
getOfflineClusters,
} from 'redux/reducers/clusters/selectors';
import { RootState } from 'lib/interfaces';
const mapStateToProps = (state: RootState) => ({
clusters: getClusterList(state),
onlineClusters: getOnlineClusters(state),
offlineClusters: getOfflineClusters(state),
});
export default connect(mapStateToProps)(ClustersWidget);

View file

@ -0,0 +1,17 @@
import React from 'react';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer';
const Dashboard: React.FC = () => (
<div className="section">
<div className="level">
<div className="level-item level-left">
<Breadcrumb>Dashboard</Breadcrumb>
</div>
</div>
<ClustersWidgetContainer />
</div>
);
export default Dashboard;

View file

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Cluster, RootState, FetchStatus } from 'lib/interfaces'; import { Cluster, RootState, FetchStatus, ClusterStatus } from 'lib/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors'; import { createFetchingSelector } from 'redux/reducers/loader/selectors';
const clustersState = ({ clusters }: RootState): Cluster[] => clusters; const clustersState = ({ clusters }: RootState): Cluster[] => clusters;
@ -12,3 +12,17 @@ export const getIsClusterListFetched = createSelector(
); );
export const getClusterList = createSelector(clustersState, (clusters) => clusters); export const getClusterList = createSelector(clustersState, (clusters) => clusters);
export const getOnlineClusters = createSelector(
getClusterList,
(clusters) => clusters.filter(
({ status }) => status === ClusterStatus.Online,
),
);
export const getOfflineClusters = createSelector(
getClusterList,
(clusters) => clusters.filter(
({ status }) => status === ClusterStatus.Offline,
),
);

View file

@ -7,11 +7,19 @@
@import "../../node_modules/bulma/sass/layout/_all.sass"; @import "../../node_modules/bulma/sass/layout/_all.sass";
@import "../../node_modules/bulma-switch/src/sass/index.sass"; @import "../../node_modules/bulma-switch/src/sass/index.sass";
.has-text-overflow-ellipsis { .has {
flex: 1; &-text-overflow-ellipsis {
overflow: hidden; flex: 1;
text-overflow: ellipsis; overflow: hidden;
white-space: nowrap; text-overflow: ellipsis;
white-space: nowrap;
}
&-margin {
&-right {
margin-right: 10px;
}
}
} }
.breadcrumb li { .breadcrumb li {
@ -32,6 +40,30 @@
width: 100%; width: 100%;
} }
.notification {
&.is-light {
&.is-primary {
background-color: #ebfffc;
color: #00947e;
}
&.is-danger {
background-color: #feecf0;
color: #cc0f35;
}
}
}
.box {
&.is-hoverable {
cursor: pointer;
&:hover {
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.2), 0 0px 0 1px rgba(10, 10, 10, 0.02);
}
}
}
@keyframes fadein { @keyframes fadein {
from { opacity: 0; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }