github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/reports/containers/nodes/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 _ from "lodash";
    13  import Long from "long";
    14  import moment from "moment";
    15  import React from "react";
    16  import { Helmet } from "react-helmet";
    17  import { connect } from "react-redux";
    18  import { withRouter, RouteComponentProps } from "react-router-dom";
    19  
    20  import * as protos from "src/js/protos";
    21  import { refreshLiveness, refreshNodes } from "src/redux/apiReducers";
    22  import { nodesSummarySelector, NodesSummary } from "src/redux/nodes";
    23  import { AdminUIState } from "src/redux/state";
    24  import { LongToMoment } from "src/util/convert";
    25  import { FixLong } from "src/util/fixLong";
    26  import { getFilters, localityToString, NodeFilterList } from "src/views/reports/components/nodeFilterList";
    27  
    28  interface NodesOwnProps {
    29    nodesSummary: NodesSummary;
    30    refreshNodes: typeof refreshNodes;
    31    refreshLiveness: typeof refreshLiveness;
    32  }
    33  
    34  interface NodesTableRowParams {
    35    title: string;
    36    extract: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => React.ReactNode;
    37    equality?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string;
    38    cellTitle?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string;
    39  }
    40  
    41  type NodesProps = NodesOwnProps & RouteComponentProps;
    42  
    43  const dateFormat = "Y-MM-DD HH:mm:ss";
    44  const detailTimeFormat = "Y/MM/DD HH:mm:ss";
    45  
    46  const loading = (
    47    <div className="section">
    48      <h1 className="base-heading">Node Diagnostics</h1>
    49      <h2 className="base-heading">Loading cluster status...</h2>
    50    </div>
    51  );
    52  
    53  function NodeTableCell(props: { value: React.ReactNode, title: string }) {
    54    return (
    55      <td className="nodes-table__cell" title={props.title}>
    56        {props.value}
    57      </td>
    58    );
    59  }
    60  
    61  // Functions starting with "print" return a single string representation which
    62  // can be used for title, the main content or even equality comparisons.
    63  function printNodeID(status: protos.cockroach.server.status.statuspb.INodeStatus) {
    64    return `n${status.desc.node_id}`;
    65  }
    66  
    67  function printSingleValue(value: string) {
    68    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
    69      return _.get(status, value, null);
    70    };
    71  }
    72  
    73  function printSingleValueWithFunction(value: string, fn: (item: any) => string) {
    74    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
    75      return fn(_.get(status, value, null));
    76    };
    77  }
    78  
    79  function printMultiValue(value: string) {
    80    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
    81      return _.join(_.get(status, value, []), "\n");
    82    };
    83  }
    84  
    85  function printDateValue(value: string, inputDateFormat: string) {
    86    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
    87      if (!_.has(status, value)) {
    88        return null;
    89      }
    90      return moment(_.get(status, value), inputDateFormat).format(dateFormat);
    91    };
    92  }
    93  
    94  function printTimestampValue(value: string) {
    95    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
    96      if (!_.has(status, value)) {
    97        return null;
    98      }
    99      return LongToMoment(FixLong(_.get(status, value) as Long)).format(dateFormat);
   100    };
   101  }
   102  
   103  // Functions starting with "title" are used exclusively to print the cell
   104  // titles. They always return a single string.
   105  function titleDateValue(value: string, inputDateFormat: string) {
   106    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
   107      if (!_.has(status, value)) {
   108        return null;
   109      }
   110      const raw = _.get(status, value);
   111      return `${moment(raw, inputDateFormat).format(dateFormat)}\n${raw}`;
   112    };
   113  }
   114  
   115  function titleTimestampValue(value: string) {
   116    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
   117      if (!_.has(status, value)) {
   118        return null;
   119      }
   120      const raw = FixLong(_.get(status, value) as Long);
   121      return `${LongToMoment(raw).format(dateFormat)}\n${raw.toString()}`;
   122    };
   123  }
   124  
   125  // Functions starting with "extract" are used exclusively for for extracting
   126  // the main content of a cell.
   127  function extractMultiValue(value: string) {
   128    return function (status: protos.cockroach.server.status.statuspb.INodeStatus) {
   129      const items = _.map(_.get(status, value, []), item => item.toString());
   130      return (
   131        <ul className="nodes-entries-list">
   132          {
   133            _.map(items, (item, key) => (
   134              <li key={key} className="nodes-entries-list--item">
   135                {item}
   136              </li>
   137            ))
   138          }
   139        </ul>
   140      );
   141    };
   142  }
   143  
   144  function extractCertificateLink(status: protos.cockroach.server.status.statuspb.INodeStatus) {
   145    const nodeID = status.desc.node_id;
   146    return (
   147      <a className="debug-link" href={`#/reports/certificates/${nodeID}`}>
   148        n{nodeID} Certificates
   149      </a>
   150    );
   151  }
   152  
   153  const nodesTableRows: NodesTableRowParams[] = [
   154    {
   155      title: "Node ID",
   156      extract: printNodeID,
   157    },
   158    {
   159      title: "Address",
   160      extract: printSingleValue("desc.address.address_field"),
   161      cellTitle: printSingleValue("desc.address.address_field"),
   162    },
   163    {
   164      title: "Locality",
   165      extract: printSingleValueWithFunction("desc.locality", localityToString),
   166      cellTitle: printSingleValueWithFunction("desc.locality", localityToString),
   167    },
   168    {
   169      title: "Certificates",
   170      extract: extractCertificateLink,
   171    },
   172    {
   173      title: "Attributes",
   174      extract: extractMultiValue("desc.attrs.attrs"),
   175      cellTitle: printMultiValue("desc.attrs.attrs"),
   176    },
   177    {
   178      title: "Environment",
   179      extract: extractMultiValue("env"),
   180      cellTitle: printMultiValue("env"),
   181    },
   182    {
   183      title: "Arguments",
   184      extract: extractMultiValue("args"),
   185      cellTitle: printMultiValue("args"),
   186    },
   187    {
   188      title: "Tag",
   189      extract: printSingleValue("build_info.tag"),
   190      cellTitle: printSingleValue("build_info.tag"),
   191      equality: printSingleValue("build_info.tag"),
   192    },
   193    {
   194      title: "Revision",
   195      extract: printSingleValue("build_info.revision"),
   196      cellTitle: printSingleValue("build_info.revision"),
   197      equality: printSingleValue("build_info.revision"),
   198    },
   199    {
   200      title: "Time",
   201      extract: printDateValue("build_info.time", detailTimeFormat),
   202      cellTitle: titleDateValue("build_info.time", detailTimeFormat),
   203      equality: printDateValue("build_info.time", detailTimeFormat),
   204    },
   205    {
   206      title: "Type",
   207      extract: printSingleValue("build_info.type"),
   208      cellTitle: printSingleValue("build_info.type"),
   209      equality: printSingleValue("build_info.type"),
   210    },
   211    {
   212      title: "Platform",
   213      extract: printSingleValue("build_info.platform"),
   214      cellTitle: printSingleValue("build_info.platform"),
   215      equality: printSingleValue("build_info.platform"),
   216    },
   217    {
   218      title: "Go Version",
   219      extract: printSingleValue("build_info.go_version"),
   220      cellTitle: printSingleValue("build_info.go_version"),
   221      equality: printSingleValue("build_info.go_version"),
   222    },
   223    {
   224      title: "CGO",
   225      extract: printSingleValue("build_info.cgo_compiler"),
   226      cellTitle: printSingleValue("build_info.cgo_compiler"),
   227      equality: printSingleValue("build_info.cgo_compiler"),
   228    },
   229    {
   230      title: "Distribution",
   231      extract: printSingleValue("build_info.distribution"),
   232      cellTitle: printSingleValue("build_info.distribution"),
   233      equality: printSingleValue("build_info.distribution"),
   234    },
   235    {
   236      title: "Started at",
   237      extract: printTimestampValue("started_at"),
   238      cellTitle: titleTimestampValue("started_at"),
   239    },
   240    {
   241      title: "Updated at",
   242      extract: printTimestampValue("updated_at"),
   243      cellTitle: titleTimestampValue("updated_at"),
   244    },
   245  ];
   246  
   247  /**
   248   * Renders the Nodes Diagnostics Report page.
   249   */
   250  export class Nodes extends React.Component<NodesProps, {}> {
   251    refresh(props = this.props) {
   252      props.refreshLiveness();
   253      props.refreshNodes();
   254    }
   255  
   256    componentDidMount() {
   257      // Refresh nodes status query when mounting.
   258      this.refresh();
   259    }
   260  
   261    componentDidUpdate(prevProps: NodesProps) {
   262      if (!_.isEqual(this.props.location, prevProps.location)) {
   263        this.refresh(this.props);
   264      }
   265    }
   266  
   267    renderNodesTableRow(
   268      orderedNodeIDs: string[],
   269      key: number,
   270      title: string,
   271      extract: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => React.ReactNode,
   272      equality?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string,
   273      cellTitle?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string,
   274    ) {
   275      const inconsistent = !_.isNil(equality) && _.chain(orderedNodeIDs)
   276        .map(nodeID => this.props.nodesSummary.nodeStatusByID[nodeID])
   277        .map(status => equality(status))
   278        .uniq()
   279        .value()
   280        .length > 1;
   281      const headerClassName = classNames(
   282        "nodes-table__cell",
   283        "nodes-table__cell--header",
   284        { "nodes-table__cell--header-warning": inconsistent },
   285      );
   286  
   287      return (
   288        <tr className="nodes-table__row" key={key}>
   289          <th className={headerClassName}>
   290            {title}
   291          </th>
   292          {
   293            _.map(orderedNodeIDs, nodeID => {
   294              const status = this.props.nodesSummary.nodeStatusByID[nodeID];
   295              return (
   296                <NodeTableCell
   297                  key={nodeID}
   298                  value={extract(status)}
   299                  title={_.isNil(cellTitle) ? null : cellTitle(status)}
   300                />
   301              );
   302            })
   303          }
   304        </tr>
   305      );
   306    }
   307  
   308    render() {
   309      const { nodesSummary } = this.props;
   310      const { nodeStatusByID } = nodesSummary;
   311      if (_.isEmpty(nodesSummary.nodeIDs)) {
   312        return loading;
   313      }
   314  
   315      const filters = getFilters(this.props.location);
   316  
   317      let nodeIDsContext = _.chain(nodesSummary.nodeIDs)
   318        .map((nodeID: string) => Number.parseInt(nodeID, 10));
   319      if (!_.isNil(filters.nodeIDs) && filters.nodeIDs.size > 0) {
   320        nodeIDsContext = nodeIDsContext.filter(nodeID => filters.nodeIDs.has(nodeID));
   321      }
   322      if (!_.isNil(filters.localityRegex)) {
   323        nodeIDsContext = nodeIDsContext.filter(nodeID => (
   324          filters.localityRegex.test(localityToString(nodeStatusByID[nodeID.toString()].desc.locality))
   325        ));
   326      }
   327  
   328      // Sort the node IDs and then convert them back to string for lookups.
   329      const orderedNodeIDs = nodeIDsContext
   330        .orderBy(nodeID => nodeID)
   331        .map(nodeID => nodeID.toString())
   332        .value();
   333  
   334      if (_.isEmpty(orderedNodeIDs)) {
   335        return (
   336          <section className="section">
   337            <h1 className="base-heading">Node Diagnostics</h1>
   338            <NodeFilterList nodeIDs={filters.nodeIDs} localityRegex={filters.localityRegex} />
   339            <h2 className="base-heading">No nodes match the filters</h2>
   340          </section>
   341        );
   342      }
   343  
   344      return (
   345        <section className="section">
   346          <Helmet title="Node Diagnostics | Debug" />
   347          <h1 className="base-heading">Node Diagnostics</h1>
   348          <NodeFilterList nodeIDs={filters.nodeIDs} localityRegex={filters.localityRegex} />
   349          <h2 className="base-heading">Nodes</h2>
   350          <table className="nodes-table">
   351            <tbody>
   352              {
   353                _.map(nodesTableRows, (row, key) => {
   354                  return this.renderNodesTableRow(
   355                    orderedNodeIDs,
   356                    key,
   357                    row.title,
   358                    row.extract,
   359                    row.equality,
   360                    row.cellTitle,
   361                  );
   362                })
   363              }
   364            </tbody>
   365          </table>
   366        </section>
   367      );
   368    }
   369  }
   370  
   371  const mapStateToProps = (state: AdminUIState) => ({
   372    nodesSummary: nodesSummarySelector(state),
   373  });
   374  
   375  const mapDispatchToProps = {
   376    refreshNodes,
   377    refreshLiveness,
   378  };
   379  
   380  export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nodes));