Bläddra i källkod

[UI] Dashboard

Oleg Shuralev 5 år sedan
förälder
incheckning
fad704f377

+ 2 - 4
frontend/src/components/App.tsx

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

+ 59 - 0
frontend/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx

@@ -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;

+ 77 - 0
frontend/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx

@@ -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;

+ 16 - 0
frontend/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts

@@ -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);

+ 17 - 0
frontend/src/components/Dashboard/Dashboard.tsx

@@ -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;

+ 15 - 1
frontend/src/redux/reducers/clusters/selectors.ts

@@ -1,5 +1,5 @@
 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';
 
 const clustersState = ({ clusters }: RootState): Cluster[] => clusters;
@@ -12,3 +12,17 @@ export const getIsClusterListFetched = createSelector(
 );
 
 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,
+  ),
+);

+ 37 - 5
frontend/src/theme/bulma_overrides.scss

@@ -7,11 +7,19 @@
 @import "../../node_modules/bulma/sass/layout/_all.sass";
 @import "../../node_modules/bulma-switch/src/sass/index.sass";
 
-.has-text-overflow-ellipsis {
-  flex: 1;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+.has {
+  &-text-overflow-ellipsis {
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &-margin {
+    &-right {
+      margin-right: 10px;
+    }
+  }
 }
 
 .breadcrumb li {
@@ -32,6 +40,30 @@
   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 {
   from { opacity: 0; }
   to   { opacity: 1; }