github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/reports/containers/statementDiagnosticsHistory/index.tsx (about) 1 // Copyright 2020 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 React from "react"; 12 import { Helmet } from "react-helmet"; 13 import { connect } from "react-redux"; 14 import moment from "moment"; 15 import { Action, Dispatch } from "redux"; 16 import Long from "long"; 17 import { isUndefined } from "lodash"; 18 import { Link } from "react-router-dom"; 19 20 import { 21 Button, 22 ColumnsConfig, 23 DownloadFile, 24 DownloadFileRef, 25 Table, 26 Text, 27 TextTypes, 28 Tooltip, 29 } from "src/components"; 30 import HeaderSection from "src/views/shared/components/headerSection"; 31 import { AdminUIState } from "src/redux/state"; 32 import { getStatementDiagnostics } from "src/util/api"; 33 import { trustIcon } from "src/util/trust"; 34 import DownloadIcon from "!!raw-loader!assets/download.svg"; 35 import { 36 selectStatementByFingerprint, 37 selectStatementDiagnosticsReports, 38 } from "src/redux/statements/statementsSelectors"; 39 import { 40 invalidateStatementDiagnosticsRequests, 41 refreshStatementDiagnosticsRequests, 42 refreshStatements, 43 } from "src/redux/apiReducers"; 44 import { DiagnosticStatusBadge } from "src/views/statements/diagnostics/diagnosticStatusBadge"; 45 import "./statementDiagnosticsHistoryView.styl"; 46 import { cockroach } from "src/js/protos"; 47 import IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; 48 import StatementDiagnosticsRequest = cockroach.server.serverpb.StatementDiagnosticsRequest; 49 import { 50 getDiagnosticsStatus, 51 sortByCompletedField, 52 sortByRequestedAtField, 53 sortByStatementFingerprintField, 54 } from "src/views/statements/diagnostics"; 55 import { trackDownloadDiagnosticsBundle } from "src/util/analytics"; 56 import { shortStatement } from "src/views/statements/statementsTable"; 57 import { summarize } from "src/util/sql/summarize"; 58 59 type StatementDiagnosticsHistoryViewProps = MapStateToProps & MapDispatchToProps; 60 61 const StatementColumn: React.FC<{ fingerprint: string }> = ({ fingerprint }) => { 62 const summary = summarize(fingerprint); 63 const shortenedStatement = shortStatement(summary, fingerprint); 64 const showTooltip = fingerprint !== shortenedStatement; 65 66 if (showTooltip) { 67 return ( 68 <Text textType={TextTypes.Code}> 69 <Tooltip 70 placement="bottom" 71 title={ 72 <pre className="cl-table-link__description">{ fingerprint }</pre> 73 } 74 overlayClassName="cl-table-link__statement-tooltip--fixed-width" 75 > 76 {shortenedStatement} 77 </Tooltip> 78 </Text> 79 ); 80 } 81 return ( 82 <Text textType={TextTypes.Code}>{shortenedStatement}</Text> 83 ); 84 }; 85 86 class StatementDiagnosticsHistoryView extends React.Component<StatementDiagnosticsHistoryViewProps> { 87 columns: ColumnsConfig<IStatementDiagnosticsReport> = [ 88 { 89 key: "activatedOn", 90 title: "Activated on", 91 sorter: sortByRequestedAtField, 92 defaultSortOrder: "descend", 93 width: "240px", 94 render: (_text, record) => { 95 const timestamp = record.requested_at.seconds.toNumber() * 1000; 96 return moment(timestamp).format("LL[ at ]h:mm a"); 97 }, 98 }, 99 { 100 key: "statement", 101 title: "statement", 102 sorter: sortByStatementFingerprintField, 103 render: (_text, record) => { 104 const { getStatementByFingerprint } = this.props; 105 const fingerprint = record.statement_fingerprint; 106 const statement = getStatementByFingerprint(fingerprint); 107 const { implicit_txn: implicitTxn = "true", query } = statement?.key?.key_data || {}; 108 109 if (isUndefined(query)) { 110 return <StatementColumn fingerprint={fingerprint} />; 111 } 112 113 return ( 114 <Link 115 to={ `/statement/${implicitTxn}/${encodeURIComponent(query)}` } 116 className="crl-statements-diagnostics-view__statements-link" 117 > 118 <StatementColumn fingerprint={fingerprint} /> 119 </Link> 120 ); 121 }, 122 }, 123 { 124 key: "status", 125 title: "status", 126 sorter: sortByCompletedField, 127 width: "160px", 128 render: (_text, record) => ( 129 <Text> 130 <DiagnosticStatusBadge 131 status={getDiagnosticsStatus(record)} 132 /> 133 </Text> 134 ), 135 }, 136 { 137 key: "actions", 138 title: "", 139 sorter: false, 140 width: "160px", 141 render: (_text, record) => { 142 if (record.completed) { 143 return ( 144 <div className="crl-statements-diagnostics-view__actions-column cell--show-on-hover nodes-table__link"> 145 <a href={`_admin/v1/stmtbundle/${record.statement_diagnostics_id}`} 146 onClick={() => trackDownloadDiagnosticsBundle(record.statement_fingerprint)}> 147 <Button 148 size="small" 149 type="flat" 150 iconPosition="left" 151 icon={() => ( 152 <span 153 className="crl-statements-diagnostics-view__icon" 154 dangerouslySetInnerHTML={ trustIcon(DownloadIcon) } 155 /> 156 )} 157 > 158 Bundle (.zip) 159 </Button> 160 </a> 161 </div> 162 ); 163 } 164 return null; 165 }, 166 }, 167 ]; 168 169 tablePageSize = 16; 170 171 downloadRef = React.createRef<DownloadFileRef>(); 172 173 constructor(props: StatementDiagnosticsHistoryViewProps) { 174 super(props); 175 props.refresh(); 176 } 177 178 renderTableTitle = () => { 179 const { diagnosticsReports } = this.props; 180 const totalCount = diagnosticsReports.length; 181 182 if (totalCount === 0) { 183 return null; 184 } 185 186 if (totalCount <= this.tablePageSize) { 187 return ( 188 <div className="diagnostics-history-view__table-header"> 189 <Text>{`${totalCount} traces`}</Text> 190 </div> 191 ); 192 } 193 194 return ( 195 <div className="diagnostics-history-view__table-header"> 196 <Text>{`${this.tablePageSize} of ${totalCount} traces`}</Text> 197 </div> 198 ); 199 } 200 201 getStatementDiagnostics = async (diagnosticsId: Long) => { 202 const request = new StatementDiagnosticsRequest({ statement_diagnostics_id: diagnosticsId }); 203 const response = await getStatementDiagnostics(request); 204 const trace = response.diagnostics?.trace; 205 this.downloadRef.current?.download("statement-diagnostics.json", "application/json", trace); 206 } 207 208 render() { 209 const { diagnosticsReports } = this.props; 210 const dataSource = diagnosticsReports.map((diagnosticsReport, idx) => ({ 211 ...diagnosticsReport, 212 key: idx, 213 })); 214 215 return ( 216 <section className="section"> 217 <Helmet title="Statement diagnostics history | Debug" /> 218 <HeaderSection 219 title="Statement diagnostics history" 220 navigationBackConfig={{ 221 text: "Advanced Debug", 222 path: "/debug", 223 }} 224 /> 225 { this.renderTableTitle() } 226 <div className="diagnostics-history-view__table-container"> 227 <Table 228 pageSize={this.tablePageSize} 229 dataSource={dataSource} 230 columns={this.columns} 231 /> 232 </div> 233 <DownloadFile ref={this.downloadRef}/> 234 </section> 235 ); 236 } 237 } 238 239 interface MapStateToProps { 240 diagnosticsReports: IStatementDiagnosticsReport[]; 241 getStatementByFingerprint: (fingerprint: string) => ReturnType<typeof selectStatementByFingerprint>; 242 } 243 244 interface MapDispatchToProps { 245 refresh: () => void; 246 } 247 248 const mapStateToProps = (state: AdminUIState): MapStateToProps => ({ 249 diagnosticsReports: selectStatementDiagnosticsReports(state) || [], 250 getStatementByFingerprint: (fingerprint: string) => selectStatementByFingerprint(state, fingerprint), 251 }); 252 253 const mapDispatchToProps = (dispatch: Dispatch<Action, AdminUIState>): MapDispatchToProps => ({ 254 refresh: () => { 255 dispatch(invalidateStatementDiagnosticsRequests()); 256 dispatch(refreshStatementDiagnosticsRequests()); 257 dispatch(refreshStatements()); 258 }, 259 }); 260 261 export default connect< 262 MapStateToProps, 263 MapDispatchToProps, 264 StatementDiagnosticsHistoryViewProps 265 >(mapStateToProps, mapDispatchToProps)(StatementDiagnosticsHistoryView);