github.com/minio/console@v1.4.1/web-app/src/screens/Console/Speedtest/Speedtest.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 { useSelector } from "react-redux"; 19 import { useNavigate } from "react-router-dom"; 20 import { 21 Box, 22 Button, 23 Grid, 24 HelpBox, 25 InputBox, 26 Loader, 27 PageLayout, 28 SpeedtestIcon, 29 WarnIcon, 30 } from "mds"; 31 import { DateTime } from "luxon"; 32 import STResults from "./STResults"; 33 import ProgressBarWrapper from "../Common/ProgressBarWrapper/ProgressBarWrapper"; 34 import InputUnitMenu from "../Common/FormComponents/InputUnitMenu/InputUnitMenu"; 35 import DistributedOnly from "../Common/DistributedOnly/DistributedOnly"; 36 import RegisterCluster from "../Support/RegisterCluster"; 37 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 38 import HelpMenu from "../HelpMenu"; 39 import { SecureComponent } from "../../../common/SecureComponent"; 40 import { selDistSet, setHelpName } from "../../../systemSlice"; 41 import { registeredCluster } from "../../../config"; 42 import { useAppDispatch } from "../../../store"; 43 import { wsProtocol } from "../../../utils/wsUtils"; 44 import { SpeedTestResponse } from "./types"; 45 import { 46 CONSOLE_UI_RESOURCE, 47 IAM_SCOPES, 48 } from "../../../common/SecureComponent/permissions"; 49 50 const Speedtest = () => { 51 const distributedSetup = useSelector(selDistSet); 52 const navigate = useNavigate(); 53 54 const [start, setStart] = useState<boolean>(false); 55 const [currStatus, setCurrStatus] = useState<SpeedTestResponse[] | null>( 56 null, 57 ); 58 const [size, setSize] = useState<string>("64"); 59 const [sizeUnit, setSizeUnit] = useState<string>("MB"); 60 const [duration, setDuration] = useState<string>("10"); 61 const [topDate, setTopDate] = useState<number>(0); 62 const [currentValue, setCurrentValue] = useState<number>(0); 63 const [totalSeconds, setTotalSeconds] = useState<number>(0); 64 const [speedometerValue, setSpeedometerValue] = useState<number>(0); 65 const clusterRegistered = registeredCluster(); 66 67 useEffect(() => { 68 // begin watch if bucketName in bucketList and start pressed 69 if (start) { 70 const url = new URL(window.location.toString()); 71 const isDev = process.env.NODE_ENV === "development"; 72 const port = isDev ? "9090" : url.port; 73 74 // check if we are using base path, if not this always is `/` 75 const baseLocation = new URL(document.baseURI); 76 const baseUrl = baseLocation.pathname; 77 78 const wsProt = wsProtocol(url.protocol); 79 const socket = new WebSocket( 80 `${wsProt}://${url.hostname}:${port}${baseUrl}ws/speedtest?&size=${size}${sizeUnit}&duration=${duration}s`, 81 ); 82 83 const baseDate = DateTime.now(); 84 85 const currentTime = baseDate.toUnixInteger() / 1000; 86 87 const incrementDate = 88 baseDate.plus({ seconds: parseInt("10") * 2 }).toUnixInteger() / 1000; 89 90 const totalSeconds = (incrementDate - currentTime) / 1000; 91 92 setTopDate(incrementDate); 93 setCurrentValue(currentTime); 94 setTotalSeconds(totalSeconds); 95 96 let interval: any | null = null; 97 if (socket !== null) { 98 socket.onopen = () => { 99 console.log("WebSocket Client Connected"); 100 socket.send("ok"); 101 interval = setInterval(() => { 102 socket.send("ok"); 103 }, 10 * 1000); 104 }; 105 socket.onmessage = (message: MessageEvent) => { 106 const data: SpeedTestResponse = JSON.parse(message.data.toString()); 107 108 setCurrStatus((prevStatus) => { 109 let prSt: SpeedTestResponse[] = []; 110 if (prevStatus) { 111 prSt = [...prevStatus]; 112 } 113 114 const insertData = data.servers !== 0 ? [data] : []; 115 return [...prSt, ...insertData]; 116 }); 117 118 const currTime = DateTime.now().toUnixInteger() / 1000; 119 setCurrentValue(currTime); 120 }; 121 socket.onclose = () => { 122 clearInterval(interval); 123 console.log("connection closed by server"); 124 // reset start status 125 setStart(false); 126 }; 127 return () => { 128 // close websocket on useEffect cleanup 129 socket.close(1000); 130 clearInterval(interval); 131 console.log("closing websockets"); 132 }; 133 } 134 } else { 135 // reset start status 136 setStart(false); 137 } 138 }, [size, sizeUnit, start, duration]); 139 140 useEffect(() => { 141 const actualSeconds = (topDate - currentValue) / 1000; 142 143 let percToDisplay = 100 - (actualSeconds * 100) / totalSeconds; 144 145 if (percToDisplay > 100) { 146 percToDisplay = 100; 147 } 148 149 setSpeedometerValue(percToDisplay); 150 }, [start, currentValue, topDate, totalSeconds]); 151 152 const stoppedLabel = currStatus !== null ? "Retest" : "Start"; 153 154 const buttonLabel = start ? "Start" : stoppedLabel; 155 156 const startSpeedtestButton = () => { 157 if (!clusterRegistered) { 158 navigate("/support/register"); 159 return; 160 } 161 162 setCurrStatus(null); 163 setStart(true); 164 }; 165 166 const dispatch = useAppDispatch(); 167 useEffect(() => { 168 dispatch(setHelpName("performance")); 169 // eslint-disable-next-line react-hooks/exhaustive-deps 170 }, []); 171 172 return ( 173 <Fragment> 174 <PageHeaderWrapper label="Performance" actions={<HelpMenu />} /> 175 176 <PageLayout> 177 {!clusterRegistered && <RegisterCluster compactMode />} 178 {!distributedSetup ? ( 179 <DistributedOnly 180 iconComponent={<SpeedtestIcon />} 181 entity={"Speedtest"} 182 /> 183 ) : ( 184 <SecureComponent 185 scopes={[IAM_SCOPES.ADMIN_HEAL]} 186 resource={CONSOLE_UI_RESOURCE} 187 > 188 <Box withBorders> 189 <Grid container> 190 <Grid item md={3} sm={12}> 191 <Box 192 sx={{ 193 fontSize: 13, 194 marginBottom: 8, 195 }} 196 > 197 {start ? ( 198 <Fragment> 199 Speedtest in progress... 200 <Loader style={{ width: 15, height: 15 }} /> 201 </Fragment> 202 ) : ( 203 <Fragment> 204 {currStatus && !start ? ( 205 <b>Speed Test results:</b> 206 ) : ( 207 <b>Performance test</b> 208 )} 209 </Fragment> 210 )} 211 </Box> 212 <Box> 213 <ProgressBarWrapper 214 value={speedometerValue} 215 ready={currStatus !== null && !start} 216 indeterminate={start} 217 size={"small"} 218 /> 219 </Box> 220 </Grid> 221 <Grid item md={4} sm={12}> 222 <div style={{ marginLeft: 10, width: 300 }}> 223 <InputBox 224 id={"size"} 225 name={"size"} 226 label={"Object Size"} 227 onChange={(e) => { 228 setSize(e.target.value); 229 }} 230 noLabelMinWidth={true} 231 value={size} 232 disabled={start || !clusterRegistered} 233 overlayObject={ 234 <InputUnitMenu 235 id={"size-unit"} 236 onUnitChange={setSizeUnit} 237 unitSelected={sizeUnit} 238 unitsList={[ 239 { label: "KiB", value: "KiB" }, 240 { label: "MiB", value: "MiB" }, 241 { label: "GiB", value: "GiB" }, 242 ]} 243 disabled={start || !clusterRegistered} 244 /> 245 } 246 /> 247 </div> 248 </Grid> 249 <Grid item md={4} sm={12}> 250 <div style={{ marginLeft: 10, width: 300 }}> 251 <InputBox 252 id={"duration"} 253 name={"duration"} 254 label={"Duration"} 255 onChange={(e) => { 256 if (e.target.validity.valid) { 257 setDuration(e.target.value); 258 } 259 }} 260 noLabelMinWidth={true} 261 value={duration} 262 disabled={start || !clusterRegistered} 263 overlayObject={ 264 <InputUnitMenu 265 id={"size-unit"} 266 onUnitChange={() => {}} 267 unitSelected={"s"} 268 unitsList={[{ label: "s", value: "s" }]} 269 disabled={start || !clusterRegistered} 270 /> 271 } 272 pattern={"[0-9]*"} 273 /> 274 </div> 275 </Grid> 276 <Grid item md={1} sm={12} sx={{ textAlign: "center" }}> 277 <Button 278 onClick={startSpeedtestButton} 279 color="primary" 280 type="button" 281 id={"start-speed-test"} 282 variant={ 283 clusterRegistered && currStatus !== null && !start 284 ? "callAction" 285 : "regular" 286 } 287 disabled={ 288 duration.trim() === "" || 289 size.trim() === "" || 290 start || 291 !clusterRegistered 292 } 293 label={buttonLabel} 294 /> 295 </Grid> 296 </Grid> 297 <Grid container> 298 <Grid item xs={12}> 299 <Fragment> 300 <Grid item xs={12}> 301 {currStatus !== null && ( 302 <Fragment> 303 <STResults results={currStatus} start={start} /> 304 </Fragment> 305 )} 306 </Grid> 307 </Fragment> 308 </Grid> 309 </Grid> 310 </Box> 311 312 {!start && !currStatus && clusterRegistered && ( 313 <Fragment> 314 <br /> 315 <HelpBox 316 title={ 317 "During the speed test all your production traffic will be temporarily suspended." 318 } 319 iconComponent={<WarnIcon />} 320 help={<Fragment />} 321 /> 322 </Fragment> 323 )} 324 </SecureComponent> 325 )} 326 </PageLayout> 327 </Fragment> 328 ); 329 }; 330 331 export default Speedtest;