github.com/minio/console@v1.4.1/web-app/src/screens/Console/Speedtest/STResults.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, useState } from "react"; 18 import get from "lodash/get"; 19 import { 20 Button, 21 ComputerLineIcon, 22 DownloadIcon, 23 DownloadStatIcon, 24 JSONIcon, 25 StorageIcon, 26 UploadStatIcon, 27 VersionIcon, 28 Grid, 29 Box, 30 } from "mds"; 31 import { IndvServerMetric, SpeedTestResponse, STServer } from "./types"; 32 import { calculateBytes, prettyNumber } from "../../../common/utils"; 33 import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts"; 34 import { cleanMetrics } from "./utils"; 35 import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper"; 36 import SpeedTestUnit from "./SpeedTestUnit"; 37 import styled from "styled-components"; 38 39 const STResultsContainer = styled.div(({ theme }) => ({ 40 "& .actionButtons": { 41 textAlign: "right", 42 }, 43 "& .descriptorLabel": { 44 fontWeight: "bold", 45 fontSize: 14, 46 }, 47 "& .resultsContainer": { 48 backgroundColor: get(theme, "boxBackground", "#FBFAFA"), 49 borderTop: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`, 50 marginTop: 30, 51 padding: 25, 52 }, 53 "& .resultsIcon": { 54 display: "flex", 55 alignItems: "center", 56 "& svg": { 57 fill: get(theme, `screenTitle.iconColor`, "#07193E"), 58 }, 59 }, 60 "& .detailedItem": { 61 display: "flex", 62 alignItems: "center", 63 justifyContent: "flex-start", 64 }, 65 "& .detailedVersion": { 66 display: "flex", 67 alignItems: "center", 68 justifyContent: "flex-end", 69 }, 70 "& .serversTable": { 71 width: "100%", 72 marginTop: 15, 73 "& thead > tr > th": { 74 textAlign: "left", 75 padding: 15, 76 fontSize: 14, 77 fontWeight: "bold", 78 }, 79 "& tbody > tr": { 80 "&:last-of-type": { 81 "& > td": { 82 borderBottom: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`, 83 }, 84 }, 85 "& > td": { 86 borderTop: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`, 87 padding: 15, 88 fontSize: 14, 89 "&:first-of-type": { 90 borderLeft: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`, 91 }, 92 "&:last-of-type": { 93 borderRight: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`, 94 }, 95 }, 96 }, 97 }, 98 "& .serverIcon": { 99 width: 55, 100 }, 101 "& .serverValue": { 102 width: 140, 103 }, 104 "& .serverHost": { 105 maxWidth: 540, 106 overflow: "hidden", 107 textOverflow: "ellipsis", 108 whiteSpace: "nowrap", 109 }, 110 "& .tableOverflow": { 111 overflowX: "auto", 112 paddingBottom: 15, 113 }, 114 "& .objectGeneral": { 115 marginTop: 15, 116 }, 117 "& .download": { 118 "& .min-icon": { 119 width: 35, 120 height: 35, 121 color: get(theme, "signalColors.good", "#4CCB92"), 122 }, 123 }, 124 "& .upload": { 125 "& .min-icon": { 126 width: 35, 127 height: 35, 128 color: get(theme, "signalColors.info", "#2781B0"), 129 }, 130 }, 131 "& .versionIcon": { 132 color: get(theme, `screenTitle.iconColor`, "#07193E"), 133 marginRight: 20, 134 }, 135 })); 136 137 interface ISTResults { 138 results: SpeedTestResponse[]; 139 start: boolean; 140 } 141 142 const STResults = ({ results, start }: ISTResults) => { 143 const [jsonView, setJsonView] = useState<boolean>(false); 144 145 const finalRes = results[results.length - 1] || []; 146 147 const getServers: STServer[] = get(finalRes, "GETStats.servers", []) || []; 148 const putServers: STServer[] = get(finalRes, "PUTStats.servers", []) || []; 149 150 const getThroughput = get(finalRes, "GETStats.throughputPerSec", 0); 151 const getObjects = get(finalRes, "GETStats.objectsPerSec", 0); 152 153 const putThroughput = get(finalRes, "PUTStats.throughputPerSec", 0); 154 const putObjects = get(finalRes, "PUTStats.objectsPerSec", 0); 155 156 let statJoin: IndvServerMetric[] = []; 157 158 getServers.forEach((item) => { 159 const hostName = item.endpoint; 160 const putMetric = putServers.find((item) => item.endpoint === hostName); 161 162 let itemJoin: IndvServerMetric = { 163 getUnit: "-", 164 getValue: "N/A", 165 host: item.endpoint, 166 putUnit: "-", 167 putValue: "N/A", 168 }; 169 170 if (item.err && item.err !== "") { 171 itemJoin.getError = item.err; 172 itemJoin.getUnit = "-"; 173 itemJoin.getValue = "N/A"; 174 } else { 175 const niceGet = calculateBytes(item.throughputPerSec.toString()); 176 177 itemJoin.getUnit = niceGet.unit; 178 itemJoin.getValue = niceGet.total.toString(); 179 } 180 181 if (putMetric) { 182 if (putMetric.err && putMetric.err !== "") { 183 itemJoin.putError = putMetric.err; 184 itemJoin.putUnit = "-"; 185 itemJoin.putValue = "N/A"; 186 } else { 187 const nicePut = calculateBytes(putMetric.throughputPerSec.toString()); 188 189 itemJoin.putUnit = nicePut.unit; 190 itemJoin.putValue = nicePut.total.toString(); 191 } 192 } 193 194 statJoin.push(itemJoin); 195 }); 196 197 const downloadResults = () => { 198 const date = new Date(); 199 let element = document.createElement("a"); 200 element.setAttribute( 201 "href", 202 "data:text/plain;charset=utf-8," + JSON.stringify(finalRes), 203 ); 204 element.setAttribute( 205 "download", 206 `speedtest_results-${date.toISOString()}.log`, 207 ); 208 209 element.style.display = "none"; 210 document.body.appendChild(element); 211 212 element.click(); 213 214 document.body.removeChild(element); 215 }; 216 217 const toggleJSONView = () => { 218 setJsonView(!jsonView); 219 }; 220 221 const finalResJSON = finalRes ? JSON.stringify(finalRes, null, 4) : ""; 222 const clnMetrics = cleanMetrics(results); 223 224 return ( 225 <STResultsContainer> 226 <Grid container className={"objectGeneral"}> 227 <Grid item xs={12} md={6} lg={6}> 228 <Grid container className={"objectGeneral"}> 229 <Grid item xs={12} md={6} lg={6}> 230 <SpeedTestUnit 231 icon={ 232 <div className={"download"}> 233 <DownloadStatIcon /> 234 </div> 235 } 236 title={"GET"} 237 throughput={`${getThroughput}`} 238 objects={getObjects} 239 /> 240 </Grid> 241 <Grid item xs={12} md={6} lg={6}> 242 <SpeedTestUnit 243 icon={ 244 <div className={"upload"}> 245 <UploadStatIcon /> 246 </div> 247 } 248 title={"PUT"} 249 throughput={`${putThroughput}`} 250 objects={putObjects} 251 /> 252 </Grid> 253 </Grid> 254 </Grid> 255 <Grid item xs={12} md={6} lg={6}> 256 <ResponsiveContainer width="99%"> 257 <AreaChart data={clnMetrics}> 258 <defs> 259 <linearGradient id="colorPut" x1="0" y1="0" x2="0" y2="1"> 260 <stop offset="0%" stopColor="#2781B0" stopOpacity={0.9} /> 261 <stop offset="95%" stopColor="#fff" stopOpacity={0} /> 262 </linearGradient> 263 <linearGradient id="colorGet" x1="0" y1="0" x2="0" y2="1"> 264 <stop offset="0%" stopColor="#4CCB92" stopOpacity={0.9} /> 265 <stop offset="95%" stopColor="#fff" stopOpacity={0} /> 266 </linearGradient> 267 </defs> 268 269 <CartesianGrid 270 strokeDasharray={"0 0"} 271 strokeWidth={1} 272 strokeOpacity={0.5} 273 stroke={"#F1F1F1"} 274 vertical={false} 275 /> 276 277 <Area 278 type="monotone" 279 dataKey={"get"} 280 stroke={"#4CCB92"} 281 fill={"url(#colorGet)"} 282 fillOpacity={0.3} 283 strokeWidth={2} 284 dot={false} 285 /> 286 <Area 287 type="monotone" 288 dataKey={"put"} 289 stroke={"#2781B0"} 290 fill={"url(#colorPut)"} 291 fillOpacity={0.3} 292 strokeWidth={2} 293 dot={false} 294 /> 295 </AreaChart> 296 </ResponsiveContainer> 297 </Grid> 298 </Grid> 299 <br /> 300 {clnMetrics.length > 1 && ( 301 <Fragment> 302 <Grid container> 303 <Grid item xs={12} md={6} className={"descriptorLabel"}> 304 {start ? ( 305 <Fragment>Preliminar Results:</Fragment> 306 ) : ( 307 <Fragment> 308 {jsonView ? "JSON Results:" : "Detailed Results:"} 309 </Fragment> 310 )} 311 </Grid> 312 <Grid 313 item 314 xs={12} 315 md={6} 316 sx={{ display: "flex", justifyContent: "right", gap: 8 }} 317 > 318 {!start && ( 319 <Fragment> 320 <Button 321 id={"download-results"} 322 aria-label="Download Results" 323 onClick={downloadResults} 324 icon={<DownloadIcon />} 325 /> 326 327 <Button 328 id={"toggle-json"} 329 aria-label="Toogle JSON" 330 onClick={toggleJSONView} 331 icon={<JSONIcon />} 332 /> 333 </Fragment> 334 )} 335 </Grid> 336 </Grid> 337 <Box withBorders useBackground sx={{ marginTop: 15 }}> 338 <Grid container> 339 {jsonView ? ( 340 <Fragment> 341 <CodeMirrorWrapper value={finalResJSON} onChange={() => {}} /> 342 </Fragment> 343 ) : ( 344 <Fragment> 345 <Grid 346 item 347 xs={12} 348 sm={12} 349 md={1} 350 lg={1} 351 className={"resultsIcon"} 352 > 353 <ComputerLineIcon width={45} /> 354 </Grid> 355 <Grid 356 item 357 xs={12} 358 sm={6} 359 md={3} 360 lg={2} 361 className={"detailedItem"} 362 > 363 Nodes: <strong>{finalRes.servers}</strong> 364 </Grid> 365 <Grid 366 item 367 xs={12} 368 sm={6} 369 md={3} 370 lg={2} 371 className={"detailedItem"} 372 > 373 Drives: <strong>{finalRes.disks}</strong> 374 </Grid> 375 <Grid 376 item 377 xs={12} 378 sm={6} 379 md={3} 380 lg={2} 381 className={"detailedItem"} 382 > 383 Concurrent: <strong>{finalRes.concurrent}</strong> 384 </Grid> 385 <Grid 386 item 387 xs={12} 388 sm={12} 389 md={12} 390 lg={5} 391 className={"detailedVersion"} 392 > 393 <span className={"versionIcon"}> 394 <VersionIcon /> 395 </span>{" "} 396 MinIO VERSION <strong>{finalRes.version}</strong> 397 </Grid> 398 <Grid item xs={12} className={"tableOverflow"}> 399 <table 400 className={"serversTable"} 401 cellSpacing={0} 402 cellPadding={0} 403 > 404 <thead> 405 <tr> 406 <th colSpan={2}>Servers</th> 407 <th>GET</th> 408 <th>PUT</th> 409 </tr> 410 </thead> 411 <tbody> 412 {statJoin.map((stats, index) => ( 413 <tr key={`storage-${index.toString()}`}> 414 <td className={"serverIcon"}> 415 <StorageIcon /> 416 </td> 417 <td className={"serverHost"}>{stats.host}</td> 418 {stats.getError && stats.getError !== "" ? ( 419 <td>{stats.getError}</td> 420 ) : ( 421 <Fragment> 422 <td className={"serverValue"}> 423 {prettyNumber(parseFloat(stats.getValue))} 424 425 {stats.getUnit}/s. 426 </td> 427 </Fragment> 428 )} 429 {stats.putError && stats.putError !== "" ? ( 430 <td>{stats.putError}</td> 431 ) : ( 432 <Fragment> 433 <td className={"serverValue"}> 434 {prettyNumber(parseFloat(stats.putValue))} 435 436 {stats.putUnit}/s. 437 </td> 438 </Fragment> 439 )} 440 </tr> 441 ))} 442 </tbody> 443 </table> 444 </Grid> 445 </Fragment> 446 )} 447 </Grid> 448 </Box> 449 </Fragment> 450 )} 451 </STResultsContainer> 452 ); 453 }; 454 455 export default STResults;