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;