github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/nodeGraphs/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 _ from "lodash";
    12  import React from "react";
    13  import { Helmet } from "react-helmet";
    14  import { connect } from "react-redux";
    15  import { createSelector } from "reselect";
    16  import { withRouter, RouteComponentProps } from "react-router-dom";
    17  
    18  import {
    19    nodeIDAttr, dashboardNameAttr,
    20  } from "src/util/constants";
    21  import Dropdown, { DropdownOption } from "src/views/shared/components/dropdown";
    22  import { PageConfig, PageConfigItem } from "src/views/shared/components/pageconfig";
    23  import TimeScaleDropdown from "src/views/cluster/containers/timescale";
    24  import ClusterSummaryBar from "./summaryBar";
    25  
    26  import { AdminUIState } from "src/redux/state";
    27  import { refreshNodes, refreshLiveness } from "src/redux/apiReducers";
    28  import { hoverStateSelector, HoverState, hoverOn, hoverOff } from "src/redux/hover";
    29  import { nodesSummarySelector, NodesSummary, LivenessStatus } from "src/redux/nodes";
    30  import Alerts from "src/views/shared/containers/alerts";
    31  import { MetricsDataProvider } from "src/views/shared/containers/metricDataProvider";
    32  
    33  import {
    34    GraphDashboardProps, storeIDsForNode,
    35  } from "./dashboards/dashboardUtils";
    36  
    37  import overviewDashboard from "./dashboards/overview";
    38  import runtimeDashboard from "./dashboards/runtime";
    39  import sqlDashboard from "./dashboards/sql";
    40  import storageDashboard from "./dashboards/storage";
    41  import replicationDashboard from "./dashboards/replication";
    42  import distributedDashboard from "./dashboards/distributed";
    43  import queuesDashboard from "./dashboards/queues";
    44  import requestsDashboard from "./dashboards/requests";
    45  import hardwareDashboard from "./dashboards/hardware";
    46  import changefeedsDashboard from "./dashboards/changefeeds";
    47  import { getMatchParamByName } from "src/util/query";
    48  
    49  interface GraphDashboard {
    50    label: string;
    51    component: (props: GraphDashboardProps) => React.ReactElement<any>[];
    52  }
    53  
    54  const dashboards: {[key: string]: GraphDashboard} = {
    55    "overview" : { label: "Overview", component: overviewDashboard },
    56    "hardware": { label: "Hardware", component: hardwareDashboard },
    57    "runtime" : { label: "Runtime", component: runtimeDashboard },
    58    "sql": { label: "SQL", component: sqlDashboard },
    59    "storage": { label: "Storage", component: storageDashboard },
    60    "replication": { label: "Replication", component: replicationDashboard },
    61    "distributed": { label: "Distributed", component: distributedDashboard },
    62    "queues": { label: "Queues", component: queuesDashboard },
    63    "requests": { label: "Slow Requests", component: requestsDashboard },
    64    "changefeeds": { label: "Changefeeds", component: changefeedsDashboard },
    65  };
    66  
    67  const defaultDashboard = "overview";
    68  
    69  const dashboardDropdownOptions = _.map(dashboards, (dashboard, key) => {
    70    return {
    71      value: key,
    72      label: dashboard.label,
    73    };
    74  });
    75  
    76  // The properties required by a NodeGraphs component.
    77  interface NodeGraphsOwnProps {
    78    refreshNodes: typeof refreshNodes;
    79    refreshLiveness: typeof refreshLiveness;
    80    hoverOn: typeof hoverOn;
    81    hoverOff: typeof hoverOff;
    82    nodesQueryValid: boolean;
    83    livenessQueryValid: boolean;
    84    nodesSummary: NodesSummary;
    85    hoverState: HoverState;
    86  }
    87  
    88  type NodeGraphsProps = NodeGraphsOwnProps & RouteComponentProps;
    89  
    90  /**
    91   * NodeGraphs renders the main content of the cluster graphs page.
    92   */
    93  export class NodeGraphs extends React.Component<NodeGraphsProps, {}> {
    94    /**
    95     * Selector to compute node dropdown options from the current node summary
    96     * collection.
    97     */
    98    private nodeDropdownOptions = createSelector(
    99      (summary: NodesSummary) => summary.nodeStatuses,
   100      (summary: NodesSummary) => summary.nodeDisplayNameByID,
   101      (summary: NodesSummary) => summary.livenessStatusByNodeID,
   102      (nodeStatuses, nodeDisplayNameByID, livenessStatusByNodeID): DropdownOption[] => {
   103        const base = [{value: "", label: "Cluster"}];
   104        return base.concat(
   105          _.chain(nodeStatuses)
   106            .filter(ns => livenessStatusByNodeID[ns.desc.node_id] !== LivenessStatus.DECOMMISSIONED)
   107            .map(ns => ({
   108              value: ns.desc.node_id.toString(),
   109              label: nodeDisplayNameByID[ns.desc.node_id],
   110            }))
   111            .value(),
   112        );
   113      },
   114    );
   115  
   116    refresh(props = this.props) {
   117      if (!props.nodesQueryValid) {
   118        props.refreshNodes();
   119      }
   120      if (!props.livenessQueryValid) {
   121        props.refreshLiveness();
   122      }
   123    }
   124  
   125    setClusterPath(nodeID: string, dashboardName: string) {
   126      const push = this.props.history.push;
   127      if (!_.isString(nodeID) || nodeID === "") {
   128        push(`/metrics/${dashboardName}/cluster`);
   129      } else {
   130        push(`/metrics/${dashboardName}/node/${nodeID}`);
   131      }
   132    }
   133  
   134    nodeChange = (selected: DropdownOption) => {
   135      this.setClusterPath(selected.value, getMatchParamByName(this.props.match, dashboardNameAttr));
   136    }
   137  
   138    dashChange = (selected: DropdownOption) => {
   139      this.setClusterPath(getMatchParamByName(this.props.match, nodeIDAttr), selected.value);
   140    }
   141  
   142    componentDidMount() {
   143      this.refresh();
   144    }
   145  
   146    componentDidUpdate() {
   147      this.refresh(this.props);
   148    }
   149  
   150    render() {
   151      const { match, nodesSummary } = this.props;
   152      const selectedDashboard = getMatchParamByName(match, dashboardNameAttr);
   153      const dashboard = _.has(dashboards, selectedDashboard)
   154        ? selectedDashboard
   155        : defaultDashboard;
   156  
   157      const title = dashboards[dashboard].label + " Dashboard";
   158      const selectedNode = getMatchParamByName(match, nodeIDAttr) || "";
   159      const nodeSources = (selectedNode !== "") ? [selectedNode] : null;
   160  
   161      // When "all" is the selected source, some graphs display a line for every
   162      // node in the cluster using the nodeIDs collection. However, if a specific
   163      // node is already selected, these per-node graphs should only display data
   164      // only for the selected node.
   165      const nodeIDs = nodeSources ? nodeSources : nodesSummary.nodeIDs;
   166  
   167      // If a single node is selected, we need to restrict the set of stores
   168      // queried for per-store metrics (only stores that belong to that node will
   169      // be queried).
   170      const storeSources = nodeSources ? storeIDsForNode(nodesSummary, nodeSources[0]) : null;
   171  
   172      // tooltipSelection is a string used in tooltips to reference the currently
   173      // selected nodes. This is a prepositional phrase, currently either "across
   174      // all nodes" or "on node X".
   175      const tooltipSelection = (nodeSources && nodeSources.length === 1)
   176                                ? `on node ${nodeSources[0]}`
   177                                : "across all nodes";
   178  
   179      const dashboardProps: GraphDashboardProps = {
   180        nodeIDs,
   181        nodesSummary,
   182        nodeSources,
   183        storeSources,
   184        tooltipSelection,
   185      };
   186  
   187      const forwardParams = {
   188        hoverOn: this.props.hoverOn,
   189        hoverOff: this.props.hoverOff,
   190        hoverState: this.props.hoverState,
   191      };
   192  
   193      // Generate graphs for the current dashboard, wrapping each one in a
   194      // MetricsDataProvider with a unique key.
   195      const graphs = dashboards[dashboard].component(dashboardProps);
   196      const graphComponents = _.map(graphs, (graph, idx) => {
   197        const key = `nodes.${dashboard}.${idx}`;
   198        return (
   199          <div key={key}>
   200            <MetricsDataProvider id={key}>
   201              { React.cloneElement(graph, forwardParams) }
   202            </MetricsDataProvider>
   203          </div>
   204        );
   205      });
   206  
   207      return (
   208        <div>
   209          <Helmet title={title} />
   210          <section className="section"><h1 className="base-heading">{ title }</h1></section>
   211          <PageConfig>
   212            <PageConfigItem>
   213              <Dropdown
   214                title="Graph"
   215                options={this.nodeDropdownOptions(this.props.nodesSummary)}
   216                selected={selectedNode}
   217                onChange={this.nodeChange}
   218              />
   219            </PageConfigItem>
   220            <PageConfigItem>
   221              <Dropdown
   222                title="Dashboard"
   223                options={dashboardDropdownOptions}
   224                selected={dashboard}
   225                onChange={this.dashChange}
   226                className="full-size"
   227              />
   228            </PageConfigItem>
   229            <PageConfigItem>
   230              <TimeScaleDropdown />
   231            </PageConfigItem>
   232          </PageConfig>
   233          <section className="section">
   234            <div className="l-columns">
   235              <div className="chart-group l-columns__left">
   236                { graphComponents }
   237              </div>
   238              <div className="l-columns__right">
   239                <Alerts />
   240                <ClusterSummaryBar nodesSummary={this.props.nodesSummary} nodeSources={nodeSources} />
   241              </div>
   242            </div>
   243          </section>
   244        </div>
   245      );
   246    }
   247  }
   248  
   249  export default withRouter(connect(
   250    (state: AdminUIState) => {
   251      return {
   252        nodesSummary: nodesSummarySelector(state),
   253        nodesQueryValid: state.cachedData.nodes.valid,
   254        livenessQueryValid: state.cachedData.nodes.valid,
   255        hoverState: hoverStateSelector(state),
   256      };
   257    },
   258    {
   259      refreshNodes,
   260      refreshLiveness,
   261      hoverOn,
   262      hoverOff,
   263    },
   264  )(NodeGraphs));