github.com/minio/console@v1.3.0/web-app/src/screens/Console/Trace/Trace.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 import React, { Fragment, useEffect, useState } from "react"; 18 import { DateTime } from "luxon"; 19 import { useSelector } from "react-redux"; 20 import { 21 Box, 22 breakPoints, 23 Button, 24 Checkbox, 25 DataTable, 26 FilterIcon, 27 Grid, 28 InputBox, 29 PageLayout, 30 } from "mds"; 31 import { AppState, useAppDispatch } from "../../../store"; 32 import { TraceMessage } from "./types"; 33 import { niceBytes, timeFromDate } from "../../../common/utils"; 34 import { wsProtocol } from "../../../utils/wsUtils"; 35 import { 36 setTraceStarted, 37 traceMessageReceived, 38 traceResetMessages, 39 } from "./traceSlice"; 40 import { setHelpName } from "../../../systemSlice"; 41 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 42 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 43 import HelpMenu from "../HelpMenu"; 44 45 var socket: any = null; 46 47 const Trace = () => { 48 const dispatch = useAppDispatch(); 49 50 const messages = useSelector((state: AppState) => state.trace.messages); 51 const traceStarted = useSelector( 52 (state: AppState) => state.trace.traceStarted, 53 ); 54 55 const [statusCode, setStatusCode] = useState<string>(""); 56 const [method, setMethod] = useState<string>(""); 57 const [func, setFunc] = useState<string>(""); 58 const [path, setPath] = useState<string>(""); 59 const [threshold, setThreshold] = useState<number>(0); 60 const [all, setAll] = useState<boolean>(false); 61 const [s3, setS3] = useState<boolean>(true); 62 const [internal, setInternal] = useState<boolean>(false); 63 const [storage, setStorage] = useState<boolean>(false); 64 const [os, setOS] = useState<boolean>(false); 65 const [errors, setErrors] = useState<boolean>(false); 66 67 const [toggleFilter, setToggleFilter] = useState<boolean>(false); 68 69 const startTrace = () => { 70 dispatch(traceResetMessages()); 71 const url = new URL(window.location.toString()); 72 const isDev = process.env.NODE_ENV === "development"; 73 const port = isDev ? "9090" : url.port; 74 75 let calls = `${s3 ? "s3," : ""}${internal ? "internal," : ""}${ 76 storage ? "storage," : "" 77 }${os ? "os," : ""}`; 78 79 if (all) { 80 calls = "all"; 81 } 82 // check if we are using base path, if not this always is `/` 83 const baseLocation = new URL(document.baseURI); 84 const baseUrl = baseLocation.pathname; 85 86 const wsProt = wsProtocol(url.protocol); 87 socket = new WebSocket( 88 `${wsProt}://${ 89 url.hostname 90 }:${port}${baseUrl}ws/trace?calls=${calls}&threshold=${threshold}&onlyErrors=${ 91 errors ? "yes" : "no" 92 }&statusCode=${statusCode}&method=${method}&funcname=${func}&path=${path}`, 93 ); 94 95 let interval: any | null = null; 96 if (socket !== null) { 97 socket.onopen = () => { 98 console.log("WebSocket Client Connected"); 99 dispatch(setTraceStarted(true)); 100 socket.send("ok"); 101 interval = setInterval(() => { 102 socket.send("ok"); 103 }, 10 * 1000); 104 }; 105 socket.onmessage = (message: MessageEvent) => { 106 let m: TraceMessage = JSON.parse(message.data.toString()); 107 108 m.ptime = DateTime.fromISO(m.time).toJSDate(); 109 m.key = Math.random(); 110 dispatch(traceMessageReceived(m)); 111 }; 112 socket.onclose = () => { 113 clearInterval(interval); 114 console.log("connection closed by server"); 115 dispatch(setTraceStarted(false)); 116 }; 117 return () => { 118 socket.close(1000); 119 clearInterval(interval); 120 console.log("closing websockets"); 121 setTraceStarted(false); 122 }; 123 } 124 }; 125 126 const stopTrace = () => { 127 socket.close(1000); 128 dispatch(setTraceStarted(false)); 129 }; 130 131 useEffect(() => { 132 dispatch(setHelpName("trace")); 133 // eslint-disable-next-line react-hooks/exhaustive-deps 134 }, []); 135 136 return ( 137 <Fragment> 138 <PageHeaderWrapper label={"Trace"} actions={<HelpMenu />} /> 139 140 <PageLayout> 141 <Box withBorders> 142 <Grid container> 143 <Grid 144 item 145 xs={12} 146 sx={{ 147 display: "flex", 148 flexFlow: "column", 149 150 "& .trace-Checkbox-label": { 151 fontSize: "14px", 152 fontWeight: "normal", 153 }, 154 }} 155 > 156 <Box 157 sx={{ 158 fontSize: "16px", 159 fontWeight: 600, 160 padding: "20px 0px 20px 0", 161 }} 162 > 163 Calls to Trace 164 </Box> 165 <Box 166 className={`${traceStarted ? "inactive-state" : ""}`} 167 sx={{ 168 display: "flex", 169 alignItems: "center", 170 justifyContent: "space-between", 171 }} 172 > 173 <Box 174 sx={{ 175 display: "flex", 176 flexFlow: "row", 177 "& .trace-checked-icon": { 178 border: "1px solid red", 179 }, 180 [`@media (min-width: ${breakPoints.md}px)`]: { 181 gap: 30, 182 }, 183 }} 184 > 185 <Checkbox 186 checked={all} 187 id={"all_calls"} 188 name={"all_calls"} 189 label={"All"} 190 onChange={() => { 191 setAll(!all); 192 }} 193 value={"all"} 194 disabled={traceStarted} 195 /> 196 <Checkbox 197 checked={s3 || all} 198 id={"s3_calls"} 199 name={"s3_calls"} 200 label={"S3"} 201 onChange={() => { 202 setS3(!s3); 203 }} 204 value={"s3"} 205 disabled={all || traceStarted} 206 /> 207 <Checkbox 208 checked={internal || all} 209 id={"internal_calls"} 210 name={"internal_calls"} 211 label={"Internal"} 212 onChange={() => { 213 setInternal(!internal); 214 }} 215 value={"internal"} 216 disabled={all || traceStarted} 217 /> 218 <Checkbox 219 checked={storage || all} 220 id={"storage_calls"} 221 name={"storage_calls"} 222 label={"Storage"} 223 onChange={() => { 224 setStorage(!storage); 225 }} 226 value={"storage"} 227 disabled={all || traceStarted} 228 /> 229 <Checkbox 230 checked={os || all} 231 id={"os_calls"} 232 name={"os_calls"} 233 label={"OS"} 234 onChange={() => { 235 setOS(!os); 236 }} 237 value={"os"} 238 disabled={all || traceStarted} 239 /> 240 </Box> 241 <Box 242 sx={{ 243 display: "flex", 244 alignItems: "center", 245 justifyContent: "space-between", 246 gap: "15px", 247 }} 248 > 249 <TooltipWrapper tooltip={"More filter options"}> 250 <Button 251 id={"filter-toggle"} 252 onClick={() => { 253 setToggleFilter(!toggleFilter); 254 }} 255 label={"Filters"} 256 icon={<FilterIcon />} 257 variant={"regular"} 258 className={"filters-toggle-button"} 259 style={{ 260 width: "118px", 261 background: toggleFilter ? "rgba(8, 28, 66, 0.04)" : "", 262 }} 263 /> 264 </TooltipWrapper> 265 266 {!traceStarted && ( 267 <Button 268 id={"start-trace"} 269 label={"Start"} 270 data-test-id={"trace-start-button"} 271 variant="callAction" 272 onClick={startTrace} 273 style={{ 274 width: "118px", 275 }} 276 /> 277 )} 278 {traceStarted && ( 279 <Button 280 id={"stop-trace"} 281 label={"Stop Trace"} 282 data-test-id={"trace-stop-button"} 283 variant="callAction" 284 onClick={stopTrace} 285 style={{ 286 width: "118px", 287 }} 288 /> 289 )} 290 </Box> 291 </Box> 292 </Grid> 293 {toggleFilter ? ( 294 <Box 295 useBackground 296 className={`${traceStarted ? "inactive-state" : ""}`} 297 sx={{ 298 marginTop: "25px", 299 display: "flex", 300 flexFlow: "column", 301 padding: "30px", 302 width: "100%", 303 304 "& .orient-vertical": { 305 flexFlow: "column", 306 "& label": { 307 marginBottom: "10px", 308 fontWeight: 600, 309 }, 310 "& .inputRebase": { 311 width: "90%", 312 }, 313 }, 314 315 "& .trace-Checkbox-label": { 316 fontSize: "14px", 317 fontWeight: "normal", 318 }, 319 }} 320 > 321 <Box 322 sx={{ 323 display: "flex", 324 }} 325 > 326 <InputBox 327 className="orient-vertical" 328 id="trace-status-code" 329 name="trace-status-code" 330 label="Status Code" 331 placeholder="e.g. 503" 332 value={statusCode} 333 onChange={(e) => { 334 setStatusCode(e.target.value); 335 }} 336 disabled={traceStarted} 337 /> 338 339 <InputBox 340 className="orient-vertical" 341 id="trace-function-name" 342 name="trace-function-name" 343 label="Function Name" 344 placeholder="e.g. FunctionName2055" 345 value={func} 346 onChange={(e) => { 347 setFunc(e.target.value); 348 }} 349 disabled={traceStarted} 350 /> 351 352 <InputBox 353 className="orient-vertical" 354 id="trace-method" 355 name="trace-method" 356 label="Method" 357 placeholder="e.g. Method 2056" 358 value={method} 359 onChange={(e) => { 360 setMethod(e.target.value); 361 }} 362 disabled={traceStarted} 363 /> 364 </Box> 365 <Box 366 sx={{ 367 gap: "30px", 368 display: "grid", 369 gridTemplateColumns: "2fr 1fr", 370 width: "100%", 371 marginTop: "33px", 372 }} 373 > 374 <Box 375 sx={{ 376 flex: 2, 377 width: "calc( 100% + 10px)", 378 }} 379 > 380 <InputBox 381 className="orient-vertical" 382 id="trace-path" 383 name="trace-path" 384 label="Path" 385 placeholder="e.g. my-bucket/my-prefix/*" 386 value={path} 387 onChange={(e) => { 388 setPath(e.target.value); 389 }} 390 disabled={traceStarted} 391 /> 392 </Box> 393 <Box 394 sx={{ 395 marginLeft: "15px", 396 }} 397 > 398 <InputBox 399 className="orient-vertical" 400 id="trace-fthreshold" 401 name="trace-fthreshold" 402 label="Response Threshold" 403 type="number" 404 placeholder="e.g. website.io.3249.114.12" 405 value={`${threshold}`} 406 onChange={(e) => { 407 setThreshold(parseInt(e.target.value)); 408 }} 409 disabled={traceStarted} 410 /> 411 </Box> 412 </Box> 413 <Box 414 sx={{ 415 display: "flex", 416 alignItems: "center", 417 justifyContent: "flex-start", 418 marginTop: "40px", 419 }} 420 > 421 <Checkbox 422 checked={errors} 423 id={"only_errors"} 424 name={"only_errors"} 425 label={"Display only Errors"} 426 onChange={() => { 427 setErrors(!errors); 428 }} 429 value={"only_errors"} 430 disabled={traceStarted} 431 /> 432 </Box> 433 </Box> 434 ) : null} 435 436 <Grid item xs={12}> 437 <Box 438 sx={{ 439 fontSize: "16px", 440 fontWeight: 600, 441 marginBottom: "30px", 442 marginTop: "30px", 443 }} 444 > 445 Trace Results 446 </Box> 447 </Grid> 448 <Grid item xs={12}> 449 <DataTable 450 columns={[ 451 { 452 label: "Time", 453 elementKey: "ptime", 454 renderFunction: (time: Date) => { 455 const timeParse = new Date(time); 456 return timeFromDate(timeParse); 457 }, 458 width: 100, 459 }, 460 { label: "Name", elementKey: "api" }, 461 { 462 label: "Status", 463 elementKey: "", 464 renderFunction: (fullElement: TraceMessage) => 465 `${fullElement.statusCode} ${fullElement.statusMsg}`, 466 renderFullObject: true, 467 }, 468 { 469 label: "Location", 470 elementKey: "configuration_id", 471 renderFunction: (fullElement: TraceMessage) => 472 `${fullElement.host} ${fullElement.client}`, 473 renderFullObject: true, 474 }, 475 { 476 label: "Load Time", 477 elementKey: "callStats.duration", 478 width: 150, 479 }, 480 { 481 label: "Upload", 482 elementKey: "callStats.rx", 483 renderFunction: niceBytes, 484 width: 150, 485 }, 486 { 487 label: "Download", 488 elementKey: "callStats.tx", 489 renderFunction: niceBytes, 490 width: 150, 491 }, 492 ]} 493 isLoading={false} 494 records={messages} 495 entityName="Traces" 496 idField="api" 497 customEmptyMessage={ 498 traceStarted 499 ? "No Traced elements received yet" 500 : "Trace is not started yet" 501 } 502 customPaperHeight={"calc(100vh - 292px)"} 503 autoScrollToBottom 504 /> 505 </Grid> 506 </Grid> 507 </Box> 508 </PageLayout> 509 </Fragment> 510 ); 511 }; 512 513 export default Trace;