github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/dataDistribution/replicaMatrix.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, { Component } from "react"; 13 import classNames from "classnames"; 14 15 import { 16 TreeNode, 17 TreePath, 18 layoutTreeHorizontal, 19 flatten, 20 sumValuesUnderPaths, 21 LayoutCell, 22 FlattenedNode, 23 } from "./tree"; 24 import { ToolTipWrapper } from "src/views/shared/components/toolTip"; 25 import { TimestampToMoment } from "src/util/convert"; 26 27 import { cockroach } from "src/js/protos"; 28 import NodeDescriptor$Properties = cockroach.roachpb.INodeDescriptor; 29 import { google } from "src/js/protos"; 30 import ITimestamp = google.protobuf.ITimestamp; 31 32 import "./replicaMatrix.styl"; 33 34 const DOWN_ARROW = "▼"; 35 const SIDE_ARROW = "▶"; 36 37 interface ReplicaMatrixState { 38 collapsedRows: TreePath[]; 39 collapsedCols: TreePath[]; 40 } 41 42 interface ReplicaMatrixProps { 43 cols: TreeNode<NodeDescriptor$Properties>; 44 rows: TreeNode<SchemaObject>; 45 getValue: (rowPath: TreePath, colPath: TreePath) => number; 46 } 47 48 // Amount to indent for a row each level of depth in the tree. 49 const ROW_TREE_INDENT_PX = 18; 50 // Margin for all rows in the matrix. Strangely, <th>s can't have margins 51 // applied in CSS. 52 const ROW_LEFT_MARGIN_PX = 5; 53 54 class ReplicaMatrix extends Component<ReplicaMatrixProps, ReplicaMatrixState> { 55 56 constructor(props: ReplicaMatrixProps) { 57 super(props); 58 this.state = { 59 collapsedRows: [["system"], ["defaultdb"], ["postgres"]], 60 collapsedCols: [], 61 }; 62 } 63 64 expandRow = (path: TreePath) => { 65 this.setState({ 66 collapsedRows: this.state.collapsedRows.filter((tp) => !_.isEqual(tp, path)), 67 }); 68 } 69 70 collapseRow = (path: TreePath) => { 71 this.setState({ 72 collapsedRows: [...this.state.collapsedRows, path], 73 }); 74 } 75 76 expandCol = (path: TreePath) => { 77 this.setState({ 78 collapsedCols: this.state.collapsedCols.filter((tp) => !_.isEqual(tp, path)), 79 }); 80 } 81 82 collapseCol = (path: TreePath) => { 83 this.setState({ 84 collapsedCols: [...this.state.collapsedCols, path], 85 }); 86 } 87 88 colLabel(col: LayoutCell<NodeDescriptor$Properties>): string { 89 if (col.isPlaceholder) { 90 return null; 91 } 92 93 if (col.isLeaf) { 94 return `n${col.data.node_id}`; 95 } 96 97 const arrow = col.isCollapsed ? SIDE_ARROW : DOWN_ARROW; 98 const localityLabel = col.path.length === 0 ? "Cluster" : col.path[col.path.length - 1]; 99 return `${arrow} ${localityLabel}`; 100 } 101 102 rowLabelText(row: FlattenedNode<SchemaObject>) { 103 if (row.isLeaf) { 104 return row.data.tableName; 105 } 106 107 const arrow = row.isCollapsed ? SIDE_ARROW : DOWN_ARROW; 108 const label = row.data.dbName ? `DB: ${row.data.dbName}` : "Cluster"; 109 110 return `${arrow} ${label}`; 111 } 112 113 rowLabel(row: FlattenedNode<SchemaObject>) { 114 const text = this.rowLabelText(row); 115 116 const label = ( 117 <span className={classNames("table-label", { "table-label--dropped": !!row.data.droppedAt })}> 118 {text} 119 </span> 120 ); 121 122 if (row.data.droppedAt) { 123 return ( 124 <ToolTipWrapper 125 text={ 126 <span> 127 Dropped at {TimestampToMoment(row.data.droppedAt).format()}. 128 Will eventually be garbage collected according to this schema 129 object's GC TTL. 130 </span> 131 } 132 > 133 {label} 134 </ToolTipWrapper> 135 ); 136 } else { 137 return label; 138 } 139 } 140 141 render() { 142 const { 143 cols, 144 rows, 145 getValue, 146 } = this.props; 147 const { 148 collapsedRows, 149 collapsedCols, 150 } = this.state; 151 152 const flattenedRows = flatten(rows, collapsedRows, true /* includeNodes */); 153 const headerRows = layoutTreeHorizontal(cols, collapsedCols); 154 const flattenedCols = flatten(cols, collapsedCols, false /* includeNodes */); 155 156 return ( 157 <table className="matrix"> 158 <thead> 159 {headerRows.map((row, idx) => ( 160 <tr key={idx}> 161 {idx === 0 162 ? <th className="matrix__metric-label"># Replicas</th> 163 : <th />} 164 {row.map((col) => ( 165 <th 166 key={col.path.join("/")} 167 colSpan={col.width} 168 className={classNames( 169 "matrix__column-header", 170 { "matrix__column-header--internal-node": !(col.isLeaf || col.isPlaceholder) }, 171 )} 172 onClick={() => ( 173 col.isCollapsed 174 ? this.expandCol(col.path) 175 : this.collapseCol(col.path) 176 )} 177 > 178 {this.colLabel(col)} 179 </th> 180 ))} 181 </tr> 182 ))} 183 </thead> 184 <tbody> 185 {flattenedRows.map((row) => { 186 return ( 187 <tr 188 key={row.path.join("/")} 189 className={classNames( 190 "matrix__row", 191 { "matrix__row--internal-node": !row.isLeaf }, 192 )} 193 onClick={() => ( 194 row.isCollapsed 195 ? this.expandRow(row.path) 196 : this.collapseRow(row.path) 197 )} 198 > 199 <th 200 className={classNames( 201 "matrix__row-header", 202 { "matrix__row-header--internal-node": !row.isLeaf }, 203 )} 204 style={{ paddingLeft: row.depth * ROW_TREE_INDENT_PX + ROW_LEFT_MARGIN_PX }} 205 > 206 {this.rowLabel(row)} 207 </th> 208 {flattenedCols.map((col) => { 209 return ( 210 <td 211 key={col.path.join("/")} 212 className="matrix__cell-value" 213 > 214 {row.isLeaf || row.isCollapsed 215 ? emptyIfZero(sumValuesUnderPaths(rows, cols, row.path, col.path, getValue)) 216 : null} 217 </td> 218 ); 219 })} 220 </tr> 221 ); 222 })} 223 </tbody> 224 </table> 225 ); 226 } 227 228 } 229 230 function emptyIfZero(n: number): string { 231 if (n === 0) { 232 return ""; 233 } 234 return `${n}`; 235 } 236 237 export default ReplicaMatrix; 238 239 export interface SchemaObject { 240 dbName?: string; 241 tableName?: string; 242 droppedAt?: ITimestamp; 243 }