github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/clusterOverview/index.tsx (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import classNames from "classnames";
    12  import d3 from "d3";
    13  import React from "react";
    14  import { Helmet } from "react-helmet";
    15  import { connect } from "react-redux";
    16  import { createSelector } from "reselect";
    17  
    18  import { AdminUIState } from "src/redux/state";
    19  import { nodesSummarySelector, NodesSummary } from "src/redux/nodes";
    20  import { Bytes as formatBytes } from "src/util/format";
    21  import createChartComponent from "src/views/shared/util/d3-react";
    22  import capacityChart from "./capacity";
    23  import spinner from "assets/spinner.gif";
    24  import { refreshNodes, refreshLiveness } from "src/redux/apiReducers";
    25  import EmailSubscription from "src/views/dashboard/emailSubscription";
    26  import "./cluster.styl";
    27  
    28  // tslint:disable-next-line:variable-name
    29  const CapacityChart = createChartComponent("svg", capacityChart());
    30  
    31  interface CapacityUsageProps {
    32    usedCapacity: number;
    33    usableCapacity: number;
    34  }
    35  
    36  const formatPercentage = d3.format("0.1%");
    37  
    38  function renderCapacityUsage(props: CapacityUsageProps) {
    39    const { usedCapacity, usableCapacity } = props;
    40    const usedPercentage = usableCapacity !== 0 ? usedCapacity / usableCapacity : 0;
    41    return [
    42      <h3 className="capacity-usage cluster-summary__title">Capacity Usage</h3>,
    43      <div className="capacity-usage cluster-summary__label storage-percent">Used<br />Percent</div>,
    44      <div className="capacity-usage cluster-summary__metric storage-percent">{ formatPercentage(usedPercentage) }</div>,
    45      <div className="capacity-usage cluster-summary__chart">
    46        <CapacityChart used={usedCapacity} usable={usableCapacity} />
    47      </div>,
    48      <div className="capacity-usage cluster-summary__label storage-used">Used<br />Capacity</div>,
    49      <div className="capacity-usage cluster-summary__metric storage-used">{ formatBytes(usedCapacity) }</div>,
    50      <div className="capacity-usage cluster-summary__label storage-usable">Usable<br />Capacity</div>,
    51      <div className="capacity-usage cluster-summary__metric storage-usable">{ formatBytes(usableCapacity) }</div>,
    52    ];
    53  }
    54  
    55  const mapStateToCapacityUsageProps = createSelector(
    56    nodesSummarySelector,
    57    function (nodesSummary: NodesSummary) {
    58      const { capacityUsed, capacityUsable } = nodesSummary.nodeSums;
    59      return {
    60        usedCapacity: capacityUsed,
    61        usableCapacity: capacityUsable,
    62      };
    63    },
    64  );
    65  
    66  interface NodeLivenessProps {
    67    liveNodes: number;
    68    suspectNodes: number;
    69    deadNodes: number;
    70  }
    71  
    72  function renderNodeLiveness(props: NodeLivenessProps) {
    73    const { liveNodes, suspectNodes, deadNodes } = props;
    74    const suspectClasses = classNames(
    75      "node-liveness",
    76      "cluster-summary__metric",
    77      "suspect-nodes",
    78      {
    79        "warning": suspectNodes > 0,
    80        "disabled": suspectNodes === 0,
    81      },
    82    );
    83    const deadClasses = classNames(
    84      "node-liveness",
    85      "cluster-summary__metric",
    86      "dead-nodes",
    87      {
    88        "alert": deadNodes > 0,
    89        "disabled": deadNodes === 0,
    90      },
    91    );
    92    return [
    93      <h3 className="node-liveness cluster-summary__title">Node Status</h3>,
    94      <div className="node-liveness cluster-summary__metric live-nodes">{ liveNodes }</div>,
    95      <div className="node-liveness cluster-summary__label live-nodes">Live<br />Nodes</div>,
    96      <div className={suspectClasses}>{ suspectNodes }</div>,
    97      <div className="node-liveness cluster-summary__label suspect-nodes">Suspect<br />Nodes</div>,
    98      <div className={deadClasses}>{ deadNodes }</div>,
    99      <div className="node-liveness cluster-summary__label dead-nodes">Dead<br />Nodes</div>,
   100    ];
   101  }
   102  
   103  const mapStateToNodeLivenessProps = createSelector(
   104    nodesSummarySelector,
   105    function (nodesSummary: NodesSummary) {
   106      const { nodeCounts } = nodesSummary.nodeSums;
   107      return {
   108        liveNodes: nodeCounts.healthy,
   109        suspectNodes: nodeCounts.suspect,
   110        deadNodes: nodeCounts.dead,
   111      };
   112    },
   113  );
   114  
   115  interface ReplicationStatusProps {
   116    totalRanges: number;
   117    underReplicatedRanges: number;
   118    unavailableRanges: number;
   119  }
   120  
   121  function renderReplicationStatus(props: ReplicationStatusProps) {
   122    const { totalRanges, underReplicatedRanges, unavailableRanges } = props;
   123    const underReplicatedClasses = classNames(
   124      "replication-status",
   125      "cluster-summary__metric",
   126      "under-replicated-ranges",
   127      {
   128        "warning": underReplicatedRanges > 0,
   129        "disabled": underReplicatedRanges === 0,
   130      },
   131    );
   132    const unavailableClasses = classNames(
   133      "replication-status",
   134      "cluster-summary__metric",
   135      "unavailable-ranges",
   136      {
   137        "alert": unavailableRanges > 0,
   138        "disabled": unavailableRanges === 0,
   139      },
   140    );
   141    return [
   142      <h3 className="replication-status cluster-summary__title">Replication Status</h3>,
   143      <div className="replication-status cluster-summary__metric total-ranges">{ totalRanges }</div>,
   144      <div className="replication-status cluster-summary__label total-ranges">Total<br />Ranges</div>,
   145      <div className={underReplicatedClasses}>{ underReplicatedRanges }</div>,
   146      <div className="replication-status cluster-summary__label under-replicated-ranges">Under-replicated<br />Ranges</div>,
   147      <div className={unavailableClasses}>{ unavailableRanges }</div>,
   148      <div className="replication-status cluster-summary__label unavailable-ranges">Unavailable<br />Ranges</div>,
   149    ];
   150  }
   151  
   152  const mapStateToReplicationStatusProps = createSelector(
   153    nodesSummarySelector,
   154    function (nodesSummary: NodesSummary) {
   155      const { totalRanges, underReplicatedRanges, unavailableRanges } = nodesSummary.nodeSums;
   156      return {
   157        totalRanges: totalRanges,
   158        underReplicatedRanges: underReplicatedRanges,
   159        unavailableRanges: unavailableRanges,
   160      };
   161    },
   162  );
   163  
   164  interface ClusterSummaryStateProps {
   165    capacityUsage: CapacityUsageProps;
   166    nodeLiveness: NodeLivenessProps;
   167    replicationStatus: ReplicationStatusProps;
   168    loading: boolean;
   169  }
   170  interface ClusterSummaryActionsProps {
   171    refreshLiveness: () => void;
   172    refreshNodes: () => void;
   173  }
   174  
   175  type ClusterSummaryProps = ClusterSummaryStateProps & ClusterSummaryActionsProps;
   176  
   177  class ClusterSummary extends React.Component<ClusterSummaryProps, {}> {
   178    componentDidMount() {
   179      this.refresh();
   180    }
   181  
   182    componentDidUpdate() {
   183      this.refresh();
   184    }
   185  
   186    refresh() {
   187      this.props.refreshLiveness();
   188      this.props.refreshNodes();
   189    }
   190  
   191    render() {
   192      const children = [];
   193  
   194      if (this.props.loading) {
   195        children.push(<img className="visualization__spinner" src={spinner} />);
   196      } else {
   197        children.push(
   198          ...renderCapacityUsage(this.props.capacityUsage),
   199          ...renderNodeLiveness(this.props.nodeLiveness),
   200          ...renderReplicationStatus(this.props.replicationStatus),
   201        );
   202      }
   203  
   204      return <section className="cluster-summary" children={React.Children.toArray(children)} />;
   205    }
   206  }
   207  
   208  function mapStateToClusterSummaryProps(state: AdminUIState) {
   209    return {
   210      capacityUsage: mapStateToCapacityUsageProps(state),
   211      nodeLiveness: mapStateToNodeLivenessProps(state),
   212      replicationStatus: mapStateToReplicationStatusProps(state),
   213      loading: !state.cachedData.nodes.data,
   214    };
   215  }
   216  
   217  const actions = {
   218    refreshLiveness: refreshLiveness,
   219    refreshNodes: refreshNodes,
   220  };
   221  
   222  // tslint:disable-next-line:variable-name
   223  const ClusterSummaryConnected = connect(mapStateToClusterSummaryProps, actions)(ClusterSummary);
   224  
   225  /**
   226   * Renders the main content of the cluster visualization page.
   227   */
   228  export default class ClusterOverview extends React.Component<any, any> {
   229    render() {
   230      return (
   231        <div className="cluster-page">
   232          <Helmet title="Cluster Overview" />
   233          <EmailSubscription />
   234          <section className="section cluster-overview">
   235            <ClusterSummaryConnected />
   236          </section>
   237          <section className="cluster-overview--fixed">
   238            { this.props.children }
   239          </section>
   240        </div>
   241      );
   242    }
   243  }