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;