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                    &nbsp;
   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:&nbsp;<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:&nbsp;<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:&nbsp;<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&nbsp;<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                                    &nbsp;
   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                                    &nbsp;
   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;