github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/statements/diagnostics/diagnosticsView.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 { connect } from "react-redux"; 13 import { Link } from "react-router-dom"; 14 import moment from "moment"; 15 import Long from "long"; 16 import emptyTracingBackground from "assets/statementsPage/emptyTracingBackground.svg"; 17 18 import { 19 Button, 20 Text, 21 TextTypes, 22 Table, 23 ColumnsConfig, 24 DownloadFile, 25 DownloadFileRef, 26 } from "src/components"; 27 import { AdminUIState } from "src/redux/state"; 28 import { getStatementDiagnostics } from "src/util/api"; 29 import { SummaryCard } from "src/views/shared/components/summaryCard"; 30 import { 31 selectDiagnosticsReportsByStatementFingerprint, 32 selectDiagnosticsReportsCountByStatementFingerprint, 33 } from "src/redux/statements/statementsSelectors"; 34 import { createStatementDiagnosticsReportAction } from "src/redux/statements"; 35 import { trustIcon } from "src/util/trust"; 36 37 import { DiagnosticStatusBadge } from "./diagnosticStatusBadge"; 38 import DownloadIcon from "!!raw-loader!assets/download.svg"; 39 import "./diagnosticsView.styl"; 40 import { cockroach } from "src/js/protos"; 41 import IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; 42 import StatementDiagnosticsRequest = cockroach.server.serverpb.StatementDiagnosticsRequest; 43 import { getDiagnosticsStatus, sortByCompletedField, sortByRequestedAtField } from "./diagnosticsUtils"; 44 import { statementDiagnostics } from "src/util/docs"; 45 import { createStatementDiagnosticsAlertLocalSetting } from "src/redux/alerts"; 46 import { trackActivateDiagnostics, trackDownloadDiagnosticsBundle } from "src/util/analytics"; 47 import { Empty } from "src/components/empty"; 48 49 interface DiagnosticsViewOwnProps { 50 statementFingerprint?: string; 51 } 52 53 type DiagnosticsViewProps = DiagnosticsViewOwnProps & MapStateToProps & MapDispatchToProps; 54 55 interface DiagnosticsViewState { 56 traces: { 57 [diagnosticsId: string]: string; 58 }; 59 } 60 61 export class DiagnosticsView extends React.Component<DiagnosticsViewProps, DiagnosticsViewState> { 62 columns: ColumnsConfig<IStatementDiagnosticsReport> = [ 63 { 64 key: "activatedOn", 65 title: "Activated on", 66 sorter: sortByRequestedAtField, 67 defaultSortOrder: "descend", 68 render: (_text, record) => { 69 const timestamp = record.requested_at.seconds.toNumber() * 1000; 70 return moment(timestamp).format("LL[ at ]h:mm a"); 71 }, 72 }, 73 { 74 key: "status", 75 title: "status", 76 sorter: sortByCompletedField, 77 width: "160px", 78 render: (_text, record) => { 79 const status = getDiagnosticsStatus(record); 80 return ( 81 <DiagnosticStatusBadge 82 status={status} 83 enableTooltip={status !== "READY"} 84 /> 85 ); 86 }, 87 }, 88 { 89 key: "actions", 90 title: "", 91 sorter: false, 92 width: "160px", 93 render: (_text, record) => { 94 if (record.completed) { 95 return ( 96 <div className="crl-statements-diagnostics-view__actions-column"> 97 <a href={`_admin/v1/stmtbundle/${record.statement_diagnostics_id}`} 98 onClick={() => trackDownloadDiagnosticsBundle(record.statement_fingerprint)}> 99 <Button 100 size="small" 101 type="flat" 102 iconPosition="left" 103 icon={() => ( 104 <span 105 className="crl-statements-diagnostics-view__icon" 106 dangerouslySetInnerHTML={ trustIcon(DownloadIcon) } 107 /> 108 )} 109 > 110 Bundle (.zip) 111 </Button> 112 </a> 113 </div> 114 ); 115 } 116 return null; 117 }, 118 }, 119 ]; 120 121 downloadRef = React.createRef<DownloadFileRef>(); 122 123 getStatementDiagnostics = async (diagnosticsId: Long) => { 124 const request = new StatementDiagnosticsRequest({ statement_diagnostics_id: diagnosticsId }); 125 const response = await getStatementDiagnostics(request); 126 const trace = response.diagnostics?.trace; 127 this.downloadRef.current?.download("statement-diagnostics.json", "application/json", trace); 128 } 129 130 onActivateButtonClick = () => { 131 const { activate, statementFingerprint } = this.props; 132 activate(statementFingerprint); 133 trackActivateDiagnostics(statementFingerprint); 134 } 135 136 componentWillUnmount() { 137 this.props.dismissAlertMessage(); 138 } 139 140 render() { 141 const { hasData, diagnosticsReports } = this.props; 142 143 const canRequestDiagnostics = diagnosticsReports.every(diagnostic => diagnostic.completed); 144 145 const dataSource = diagnosticsReports.map((diagnosticsReport, idx) => ({ 146 ...diagnosticsReport, 147 key: idx, 148 })); 149 150 if (!hasData) { 151 return ( 152 <SummaryCard className="summary--card__empty-state"> 153 <EmptyDiagnosticsView {...this.props} /> 154 </SummaryCard> 155 ); 156 } 157 return ( 158 <SummaryCard> 159 <div 160 className="crl-statements-diagnostics-view__title" 161 > 162 <Text 163 textType={TextTypes.Heading3} 164 > 165 Statement diagnostics 166 </Text> 167 { 168 canRequestDiagnostics && ( 169 <Button 170 onClick={this.onActivateButtonClick} 171 disabled={!canRequestDiagnostics} 172 type="secondary" 173 className="crl-statements-diagnostics-view__activate-button" 174 > 175 Activate diagnostics 176 </Button> 177 ) 178 } 179 </div> 180 <Table 181 dataSource={dataSource} 182 columns={this.columns} 183 /> 184 <div className="crl-statements-diagnostics-view__footer"> 185 <Link to="/reports/statements/diagnosticshistory">All statement diagnostics</Link> 186 </div> 187 <DownloadFile ref={this.downloadRef}/> 188 </SummaryCard> 189 ); 190 } 191 } 192 193 export const EmptyDiagnosticsView = ({ activate, statementFingerprint }: DiagnosticsViewProps) => { 194 const onActivateButtonClick = () => { 195 activate(statementFingerprint); 196 trackActivateDiagnostics(statementFingerprint); 197 }; 198 return ( 199 <Empty 200 title="Activate statement diagnostics" 201 description="When you activate statement diagnostics, CockroachDB will wait for the next query that matches 202 this statement fingerprint. A download button will appear on the statement list and detail pages 203 when the query is ready. The statement diagnostic will include EXPLAIN plans, 204 table statistics, and traces." 205 anchor="Learn More" 206 link={statementDiagnostics} 207 label="Activate" 208 onClick={onActivateButtonClick} 209 backgroundImage={emptyTracingBackground} 210 /> 211 ); 212 }; 213 214 interface MapStateToProps { 215 hasData: boolean; 216 diagnosticsReports: IStatementDiagnosticsReport[]; 217 } 218 219 interface MapDispatchToProps { 220 activate: (statementFingerprint: string) => void; 221 dismissAlertMessage: () => void; 222 } 223 224 const mapStateToProps = (state: AdminUIState, props: DiagnosticsViewProps): MapStateToProps => { 225 const { statementFingerprint } = props; 226 const hasData = selectDiagnosticsReportsCountByStatementFingerprint(state, statementFingerprint) > 0; 227 const diagnosticsReports = selectDiagnosticsReportsByStatementFingerprint(state, statementFingerprint); 228 return { 229 hasData, 230 diagnosticsReports, 231 }; 232 }; 233 234 const mapDispatchToProps: MapDispatchToProps = { 235 activate: createStatementDiagnosticsReportAction, 236 dismissAlertMessage: () => createStatementDiagnosticsAlertLocalSetting.set({ show: false }), 237 }; 238 239 export default connect< 240 MapStateToProps, 241 MapDispatchToProps, 242 DiagnosticsViewOwnProps 243 >(mapStateToProps, mapDispatchToProps)(DiagnosticsView);