github.com/minio/console@v1.4.1/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 { 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  import useWebSocket, { ReadyState } from "react-use-websocket";
    45  
    46  const Trace = () => {
    47    const dispatch = useAppDispatch();
    48  
    49    const messages = useSelector((state: AppState) => state.trace.messages);
    50    const traceStarted = useSelector(
    51      (state: AppState) => state.trace.traceStarted,
    52    );
    53  
    54    const [statusCode, setStatusCode] = useState<string>("");
    55    const [method, setMethod] = useState<string>("");
    56    const [func, setFunc] = useState<string>("");
    57    const [path, setPath] = useState<string>("");
    58    const [threshold, setThreshold] = useState<number>(0);
    59    const [all, setAll] = useState<boolean>(false);
    60    const [s3, setS3] = useState<boolean>(true);
    61    const [internal, setInternal] = useState<boolean>(false);
    62    const [storage, setStorage] = useState<boolean>(false);
    63    const [os, setOS] = useState<boolean>(false);
    64    const [errors, setErrors] = useState<boolean>(false);
    65  
    66    const [toggleFilter, setToggleFilter] = useState<boolean>(false);
    67    const [logActive, setLogActive] = useState(false);
    68    const [wsUrl, setWsUrl] = useState<string>("");
    69  
    70    useEffect(() => {
    71      const url = new URL(window.location.toString());
    72      const wsProt = wsProtocol(url.protocol);
    73      const port = process.env.NODE_ENV === "development" ? "9090" : url.port;
    74      const calls = all
    75        ? "all"
    76        : (() => {
    77            const c = [];
    78            if (s3) c.push("s3");
    79            if (internal) c.push("internal");
    80            if (storage) c.push("storage");
    81            if (os) c.push("os");
    82            return c.join(",");
    83          })();
    84  
    85      // check if we are using base path, if not this always is `/`
    86      const baseLocation = new URL(document.baseURI).pathname;
    87  
    88      const wsUrl = new URL(
    89        `${wsProt}://${url.hostname}:${port}${baseLocation}ws/trace`,
    90      );
    91      wsUrl.searchParams.append("calls", calls);
    92      wsUrl.searchParams.append("threshold", threshold.toString());
    93      wsUrl.searchParams.append("onlyErrors", errors ? "yes" : "no");
    94      wsUrl.searchParams.append("statusCode", statusCode);
    95      wsUrl.searchParams.append("method", method);
    96      wsUrl.searchParams.append("funcname", func);
    97      wsUrl.searchParams.append("path", path);
    98      setWsUrl(wsUrl.href);
    99    }, [
   100      all,
   101      s3,
   102      internal,
   103      storage,
   104      os,
   105      threshold,
   106      errors,
   107      statusCode,
   108      method,
   109      func,
   110      path,
   111    ]);
   112  
   113    const { sendMessage, lastJsonMessage, readyState } =
   114      useWebSocket<TraceMessage>(
   115        wsUrl,
   116        {
   117          heartbeat: {
   118            message: "ok",
   119            interval: 10 * 1000, // send ok every 10 seconds
   120            timeout: 365 * 24 * 60 * 60 * 1000, // disconnect after 365 days (workaround, because heartbeat gets no response)
   121          },
   122        },
   123        logActive,
   124      );
   125  
   126    useEffect(() => {
   127      if (readyState === ReadyState.CONNECTING) {
   128        dispatch(traceResetMessages());
   129      } else if (readyState === ReadyState.OPEN) {
   130        dispatch(setTraceStarted(true));
   131      } else if (readyState === ReadyState.CLOSED) {
   132        dispatch(setTraceStarted(false));
   133      }
   134    }, [readyState, dispatch, sendMessage]);
   135  
   136    useEffect(() => {
   137      if (lastJsonMessage) {
   138        lastJsonMessage.ptime = DateTime.fromISO(lastJsonMessage.time).toJSDate();
   139        lastJsonMessage.key = Math.random();
   140        dispatch(traceMessageReceived(lastJsonMessage));
   141      }
   142    }, [lastJsonMessage, dispatch]);
   143  
   144    useEffect(() => {
   145      dispatch(setHelpName("trace"));
   146      // eslint-disable-next-line react-hooks/exhaustive-deps
   147    }, []);
   148  
   149    return (
   150      <Fragment>
   151        <PageHeaderWrapper label={"Trace"} actions={<HelpMenu />} />
   152  
   153        <PageLayout>
   154          <Box withBorders>
   155            <Grid container>
   156              <Grid
   157                item
   158                xs={12}
   159                sx={{
   160                  display: "flex",
   161                  flexFlow: "column",
   162  
   163                  "& .trace-Checkbox-label": {
   164                    fontSize: "14px",
   165                    fontWeight: "normal",
   166                  },
   167                }}
   168              >
   169                <Box
   170                  sx={{
   171                    fontSize: "16px",
   172                    fontWeight: 600,
   173                    padding: "20px 0px 20px 0",
   174                  }}
   175                >
   176                  Calls to Trace
   177                </Box>
   178                <Box
   179                  className={`${traceStarted ? "inactive-state" : ""}`}
   180                  sx={{
   181                    display: "flex",
   182                    alignItems: "center",
   183                    justifyContent: "space-between",
   184                  }}
   185                >
   186                  <Box
   187                    sx={{
   188                      display: "flex",
   189                      flexFlow: "row",
   190                      "& .trace-checked-icon": {
   191                        border: "1px solid red",
   192                      },
   193                      [`@media (min-width: ${breakPoints.md}px)`]: {
   194                        gap: 30,
   195                      },
   196                    }}
   197                  >
   198                    <Checkbox
   199                      checked={all}
   200                      id={"all_calls"}
   201                      name={"all_calls"}
   202                      label={"All"}
   203                      onChange={() => setAll(!all)}
   204                      value={"all"}
   205                      disabled={traceStarted}
   206                    />
   207                    <Checkbox
   208                      checked={s3 || all}
   209                      id={"s3_calls"}
   210                      name={"s3_calls"}
   211                      label={"S3"}
   212                      onChange={() => setS3(!s3)}
   213                      value={"s3"}
   214                      disabled={all || traceStarted}
   215                    />
   216                    <Checkbox
   217                      checked={internal || all}
   218                      id={"internal_calls"}
   219                      name={"internal_calls"}
   220                      label={"Internal"}
   221                      onChange={() => setInternal(!internal)}
   222                      value={"internal"}
   223                      disabled={all || traceStarted}
   224                    />
   225                    <Checkbox
   226                      checked={storage || all}
   227                      id={"storage_calls"}
   228                      name={"storage_calls"}
   229                      label={"Storage"}
   230                      onChange={() => setStorage(!storage)}
   231                      value={"storage"}
   232                      disabled={all || traceStarted}
   233                    />
   234                    <Checkbox
   235                      checked={os || all}
   236                      id={"os_calls"}
   237                      name={"os_calls"}
   238                      label={"OS"}
   239                      onChange={() => setOS(!os)}
   240                      value={"os"}
   241                      disabled={all || traceStarted}
   242                    />
   243                  </Box>
   244                  <Box
   245                    sx={{
   246                      display: "flex",
   247                      alignItems: "center",
   248                      justifyContent: "space-between",
   249                      gap: "15px",
   250                    }}
   251                  >
   252                    <TooltipWrapper tooltip={"More filter options"}>
   253                      <Button
   254                        id={"filter-toggle"}
   255                        onClick={() => setToggleFilter(!toggleFilter)}
   256                        label={"Filters"}
   257                        icon={<FilterIcon />}
   258                        variant={"regular"}
   259                        className={"filters-toggle-button"}
   260                        style={{
   261                          width: "118px",
   262                          background: toggleFilter ? "rgba(8, 28, 66, 0.04)" : "",
   263                        }}
   264                      />
   265                    </TooltipWrapper>
   266  
   267                    {!traceStarted && (
   268                      <Button
   269                        id={"start-trace"}
   270                        label={"Start"}
   271                        data-test-id={"trace-start-button"}
   272                        variant="callAction"
   273                        onClick={() => setLogActive(true)}
   274                        style={{
   275                          width: "118px",
   276                        }}
   277                      />
   278                    )}
   279                    {traceStarted && (
   280                      <Button
   281                        id={"stop-trace"}
   282                        label={"Stop Trace"}
   283                        data-test-id={"trace-stop-button"}
   284                        variant="callAction"
   285                        onClick={() => setLogActive(false)}
   286                        style={{
   287                          width: "118px",
   288                        }}
   289                      />
   290                    )}
   291                  </Box>
   292                </Box>
   293              </Grid>
   294              {toggleFilter ? (
   295                <Box
   296                  useBackground
   297                  className={`${traceStarted ? "inactive-state" : ""}`}
   298                  sx={{
   299                    marginTop: "25px",
   300                    display: "flex",
   301                    flexFlow: "column",
   302                    padding: "30px",
   303                    width: "100%",
   304  
   305                    "& .orient-vertical": {
   306                      flexFlow: "column",
   307                      "& label": {
   308                        marginBottom: "10px",
   309                        fontWeight: 600,
   310                      },
   311                      "& .inputRebase": {
   312                        width: "90%",
   313                      },
   314                    },
   315  
   316                    "& .trace-Checkbox-label": {
   317                      fontSize: "14px",
   318                      fontWeight: "normal",
   319                    },
   320                  }}
   321                >
   322                  <Box
   323                    sx={{
   324                      display: "flex",
   325                    }}
   326                  >
   327                    <InputBox
   328                      className="orient-vertical"
   329                      id="trace-status-code"
   330                      name="trace-status-code"
   331                      label="Status Code"
   332                      placeholder="e.g. 503"
   333                      value={statusCode}
   334                      onChange={(e) => setStatusCode(e.target.value)}
   335                      disabled={traceStarted}
   336                    />
   337  
   338                    <InputBox
   339                      className="orient-vertical"
   340                      id="trace-function-name"
   341                      name="trace-function-name"
   342                      label="Function Name"
   343                      placeholder="e.g. FunctionName2055"
   344                      value={func}
   345                      onChange={(e) => setFunc(e.target.value)}
   346                      disabled={traceStarted}
   347                    />
   348  
   349                    <InputBox
   350                      className="orient-vertical"
   351                      id="trace-method"
   352                      name="trace-method"
   353                      label="Method"
   354                      placeholder="e.g. Method 2056"
   355                      value={method}
   356                      onChange={(e) => setMethod(e.target.value)}
   357                      disabled={traceStarted}
   358                    />
   359                  </Box>
   360                  <Box
   361                    sx={{
   362                      gap: "30px",
   363                      display: "grid",
   364                      gridTemplateColumns: "2fr 1fr",
   365                      width: "100%",
   366                      marginTop: "33px",
   367                    }}
   368                  >
   369                    <Box
   370                      sx={{
   371                        flex: 2,
   372                        width: "calc( 100% + 10px)",
   373                      }}
   374                    >
   375                      <InputBox
   376                        className="orient-vertical"
   377                        id="trace-path"
   378                        name="trace-path"
   379                        label="Path"
   380                        placeholder="e.g. my-bucket/my-prefix/*"
   381                        value={path}
   382                        onChange={(e) => setPath(e.target.value)}
   383                        disabled={traceStarted}
   384                      />
   385                    </Box>
   386                    <Box
   387                      sx={{
   388                        marginLeft: "15px",
   389                      }}
   390                    >
   391                      <InputBox
   392                        className="orient-vertical"
   393                        id="trace-fthreshold"
   394                        name="trace-fthreshold"
   395                        label="Response Threshold"
   396                        type="number"
   397                        placeholder="e.g. website.io.3249.114.12"
   398                        value={`${threshold}`}
   399                        onChange={(e) => setThreshold(parseInt(e.target.value))}
   400                        disabled={traceStarted}
   401                      />
   402                    </Box>
   403                  </Box>
   404                  <Box
   405                    sx={{
   406                      display: "flex",
   407                      alignItems: "center",
   408                      justifyContent: "flex-start",
   409                      marginTop: "40px",
   410                    }}
   411                  >
   412                    <Checkbox
   413                      checked={errors}
   414                      id={"only_errors"}
   415                      name={"only_errors"}
   416                      label={"Display only Errors"}
   417                      onChange={() => setErrors(!errors)}
   418                      value={"only_errors"}
   419                      disabled={traceStarted}
   420                    />
   421                  </Box>
   422                </Box>
   423              ) : null}
   424  
   425              <Grid item xs={12}>
   426                <Box
   427                  sx={{
   428                    fontSize: "16px",
   429                    fontWeight: 600,
   430                    marginBottom: "30px",
   431                    marginTop: "30px",
   432                  }}
   433                >
   434                  Trace Results
   435                </Box>
   436              </Grid>
   437              <Grid item xs={12}>
   438                <DataTable
   439                  columns={[
   440                    {
   441                      label: "Time",
   442                      elementKey: "ptime",
   443                      renderFunction: (time: Date) => {
   444                        const timeParse = new Date(time);
   445                        return timeFromDate(timeParse);
   446                      },
   447                      width: 100,
   448                    },
   449                    { label: "Name", elementKey: "api" },
   450                    {
   451                      label: "Status",
   452                      elementKey: "",
   453                      renderFunction: (fullElement: TraceMessage) =>
   454                        `${fullElement.statusCode} ${fullElement.statusMsg}`,
   455                      renderFullObject: true,
   456                    },
   457                    {
   458                      label: "Location",
   459                      elementKey: "configuration_id",
   460                      renderFunction: (fullElement: TraceMessage) =>
   461                        `${fullElement.host} ${fullElement.client}`,
   462                      renderFullObject: true,
   463                    },
   464                    {
   465                      label: "Load Time",
   466                      elementKey: "callStats.duration",
   467                      width: 150,
   468                    },
   469                    {
   470                      label: "Upload",
   471                      elementKey: "callStats.rx",
   472                      renderFunction: niceBytes,
   473                      width: 150,
   474                    },
   475                    {
   476                      label: "Download",
   477                      elementKey: "callStats.tx",
   478                      renderFunction: niceBytes,
   479                      width: 150,
   480                    },
   481                  ]}
   482                  isLoading={false}
   483                  records={messages}
   484                  entityName="Traces"
   485                  idField="api"
   486                  customEmptyMessage={
   487                    traceStarted
   488                      ? "No Traced elements received yet"
   489                      : "Trace is not started yet"
   490                  }
   491                  customPaperHeight={"calc(100vh - 292px)"}
   492                  autoScrollToBottom
   493                />
   494              </Grid>
   495            </Grid>
   496          </Box>
   497        </PageLayout>
   498      </Fragment>
   499    );
   500  };
   501  
   502  export default Trace;