github.com/minio/console@v1.4.1/web-app/src/screens/Console/Logs/LogSearch/LogsSearchMain.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, useCallback, useEffect, useState } from "react";
    18  import get from "lodash/get";
    19  import { useSelector } from "react-redux";
    20  import { CSSObject } from "styled-components";
    21  import {
    22    Box,
    23    breakPoints,
    24    Button,
    25    DataTable,
    26    ExpandOptionsButton,
    27    Grid,
    28    PageLayout,
    29    SearchIcon,
    30  } from "mds";
    31  import { DateTime } from "luxon";
    32  import { IReqInfoSearchResults, ISearchResponse } from "./types";
    33  import { niceBytes, nsToSeconds } from "../../../../common/utils";
    34  import { ErrorResponseHandler } from "../../../../common/types";
    35  import { LogSearchColumnLabels } from "./utils";
    36  import {
    37    CONSOLE_UI_RESOURCE,
    38    IAM_SCOPES,
    39  } from "../../../../common/SecureComponent/permissions";
    40  import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
    41  import { selFeatures } from "../../consoleSlice";
    42  import { useAppDispatch } from "../../../../store";
    43  import { SecureComponent } from "../../../../common/SecureComponent";
    44  import api from "../../../../common/api";
    45  import FilterInputWrapper from "../../Common/FormComponents/FilterInputWrapper/FilterInputWrapper";
    46  import LogSearchFullModal from "./LogSearchFullModal";
    47  import DateRangeSelector from "../../Common/FormComponents/DateRangeSelector/DateRangeSelector";
    48  import MissingIntegration from "../../Common/MissingIntegration/MissingIntegration";
    49  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    50  import HelpMenu from "../../HelpMenu";
    51  
    52  const filtersContainer: CSSObject = {
    53    display: "flex",
    54    justifyContent: "space-between",
    55    marginBottom: 12,
    56  };
    57  
    58  const LogsSearchMain = () => {
    59    const dispatch = useAppDispatch();
    60    const features = useSelector(selFeatures);
    61  
    62    const [loading, setLoading] = useState<boolean>(true);
    63    const [timeStart, setTimeStart] = useState<DateTime | null>(null);
    64    const [timeEnd, setTimeEnd] = useState<DateTime | null>(null);
    65    const [filterOpen, setFilterOpen] = useState<boolean>(false);
    66    const [records, setRecords] = useState<IReqInfoSearchResults[]>([]);
    67    const [bucket, setBucket] = useState<string>("");
    68    const [apiName, setApiName] = useState<string>("");
    69    const [accessKey, setAccessKey] = useState<string>("");
    70    const [userAgent, setUserAgent] = useState<string>("");
    71    const [object, setObject] = useState<string>("");
    72    const [requestID, setRequestID] = useState<string>("");
    73    const [responseStatus, setResponseStatus] = useState<string>("");
    74    const [sortOrder, setSortOrder] = useState<"ASC" | "DESC" | undefined>(
    75      "DESC",
    76    );
    77    const [columnsShown, setColumnsShown] = useState<string[]>([
    78      "time",
    79      "api_name",
    80      "access_key",
    81      "bucket",
    82      "object",
    83      "remote_host",
    84      "request_id",
    85      "user_agent",
    86      "response_status",
    87    ]);
    88    const [nextPage, setNextPage] = useState<number>(0);
    89    const [alreadyFetching, setAlreadyFetching] = useState<boolean>(false);
    90    const [logSearchExtrasOpen, setLogSearchExtrasOpen] =
    91      useState<boolean>(false);
    92    const [selectedItem, setSelectedItem] =
    93      useState<IReqInfoSearchResults | null>(null);
    94  
    95    let recordsResp: any = null;
    96    const logSearchEnabled = features && features.includes("log-search");
    97  
    98    const fetchRecords = useCallback(() => {
    99      if (!alreadyFetching && logSearchEnabled) {
   100        setAlreadyFetching(true);
   101        let queryParams = `${bucket !== "" ? `&fp=bucket:${bucket}` : ""}${
   102          object !== "" ? `&fp=object:${object}` : ""
   103        }${apiName !== "" ? `&fp=api_name:${apiName}` : ""}${
   104          accessKey !== "" ? `&fp=access_key:${accessKey}` : ""
   105        }${requestID !== "" ? `&fp=request_id:${requestID}` : ""}${
   106          userAgent !== "" ? `&fp=user_agent:${userAgent}` : ""
   107        }${responseStatus !== "" ? `&fp=response_status:${responseStatus}` : ""}`;
   108  
   109        queryParams = queryParams.trim();
   110  
   111        if (queryParams.endsWith(",")) {
   112          queryParams = queryParams.slice(0, -1);
   113        }
   114  
   115        api
   116          .invoke(
   117            "GET",
   118            `/api/v1/logs/search?q=reqinfo${
   119              queryParams !== "" ? `${queryParams}` : ""
   120            }&pageSize=100&pageNo=${nextPage}&order=${
   121              sortOrder === "DESC" ? "timeDesc" : "timeAsc"
   122            }${
   123              timeStart !== null ? `&timeStart=${timeStart.toUTC().toISO()}` : ""
   124            }${timeEnd !== null ? `&timeEnd=${timeEnd.toUTC().toISO()}` : ""}`,
   125          )
   126          .then((res: ISearchResponse) => {
   127            const fetchedResults = res.results || [];
   128  
   129            setLoading(false);
   130            setAlreadyFetching(false);
   131            setRecords(fetchedResults);
   132            setNextPage(nextPage + 1);
   133  
   134            if (recordsResp !== null) {
   135              recordsResp();
   136            }
   137          })
   138          .catch((err: ErrorResponseHandler) => {
   139            setLoading(false);
   140            setAlreadyFetching(false);
   141            dispatch(setErrorSnackMessage(err));
   142          });
   143      } else {
   144        setLoading(false);
   145        setAlreadyFetching(false);
   146      }
   147    }, [
   148      alreadyFetching,
   149      logSearchEnabled,
   150      bucket,
   151      object,
   152      apiName,
   153      accessKey,
   154      requestID,
   155      userAgent,
   156      responseStatus,
   157      nextPage,
   158      sortOrder,
   159      timeStart,
   160      timeEnd,
   161      recordsResp,
   162      dispatch,
   163    ]);
   164  
   165    useEffect(() => {
   166      if (loading) {
   167        setRecords([]);
   168        fetchRecords();
   169      }
   170    }, [loading, sortOrder, fetchRecords]);
   171  
   172    const triggerLoad = () => {
   173      setNextPage(0);
   174      setLoading(true);
   175    };
   176  
   177    const selectColumn = (colID: string) => {
   178      let newArray: string[];
   179  
   180      const columnShown = columnsShown.findIndex((item) => item === colID);
   181  
   182      // Column Exist, We remove from Array
   183      if (columnShown >= 0) {
   184        newArray = columnsShown.filter((element) => element !== colID);
   185      } else {
   186        // Column not visible, we include it in the array
   187        newArray = [...columnsShown, colID];
   188      }
   189  
   190      setColumnsShown(newArray);
   191    };
   192  
   193    const sortChange = (sortData: any) => {
   194      const newSortDirection = get(sortData, "sortDirection", "DESC");
   195      setSortOrder(newSortDirection);
   196      setNextPage(0);
   197      setLoading(true);
   198    };
   199  
   200    const loadMoreRecords = (_: { startIndex: number; stopIndex: number }) => {
   201      fetchRecords();
   202      return new Promise((resolve) => {
   203        recordsResp = resolve;
   204      });
   205    };
   206  
   207    const openExtraInformation = (item: IReqInfoSearchResults) => {
   208      setSelectedItem(item);
   209      setLogSearchExtrasOpen(true);
   210    };
   211  
   212    const closeViewExtraInformation = () => {
   213      setSelectedItem(null);
   214      setLogSearchExtrasOpen(false);
   215    };
   216  
   217    useEffect(() => {
   218      dispatch(setHelpName("audit_logs"));
   219      // eslint-disable-next-line react-hooks/exhaustive-deps
   220    }, []);
   221  
   222    return (
   223      <Fragment>
   224        {logSearchExtrasOpen && selectedItem !== null && (
   225          <LogSearchFullModal
   226            logSearchElement={selectedItem}
   227            modalOpen={logSearchExtrasOpen}
   228            onClose={closeViewExtraInformation}
   229          />
   230        )}
   231  
   232        <PageHeaderWrapper label="Audit Logs" actions={<HelpMenu />} />
   233  
   234        <PageLayout>
   235          {!logSearchEnabled ? (
   236            <MissingIntegration
   237              entity={"Audit Logs"}
   238              iconComponent={<SearchIcon />}
   239              documentationLink="https://min.io/docs/minio/windows/operations/monitoring/minio-logging.html?ref=con"
   240            />
   241          ) : (
   242            <Fragment>
   243              {" "}
   244              <Box withBorders sx={{ marginBottom: 15 }}>
   245                <Grid
   246                  item
   247                  xs={12}
   248                  sx={{
   249                    display: "flex",
   250                    padding: 15,
   251                    [`@media (max-width: ${breakPoints.lg}px)`]: {
   252                      flexFlow: "column",
   253                    },
   254                  }}
   255                >
   256                  <Box>
   257                    <DateRangeSelector
   258                      setTimeEnd={(time) => setTimeEnd(time)}
   259                      setTimeStart={(time) => setTimeStart(time)}
   260                      timeEnd={timeEnd}
   261                      timeStart={timeStart}
   262                    />
   263                  </Box>
   264                  <Box sx={{ display: "flex", alignItems: "center" }}>
   265                    <ExpandOptionsButton
   266                      label={`${filterOpen ? "Hide" : "Show"} advanced Filters`}
   267                      open={filterOpen}
   268                      onClick={() => {
   269                        setFilterOpen(!filterOpen);
   270                      }}
   271                    />
   272                  </Box>
   273                </Grid>
   274                <Grid
   275                  item
   276                  xs={12}
   277                  sx={{
   278                    display: filterOpen ? "block" : "none",
   279                    overflowY: "hidden",
   280                    marginBottom: filterOpen ? 12 : 0,
   281                  }}
   282                >
   283                  <Box
   284                    sx={{
   285                      marginLeft: 15,
   286                      marginBottom: 15,
   287                      fontSize: 12,
   288                      color: "#9C9C9C",
   289                    }}
   290                  >
   291                    Enable your preferred options to get filtered records.
   292                    <br />
   293                    You can use '*' to match any character, '.' to signify a
   294                    single character or '\' to scape an special character (E.g.
   295                    mybucket-*)
   296                  </Box>
   297                  <Box sx={filtersContainer}>
   298                    <FilterInputWrapper
   299                      onChange={setBucket}
   300                      value={bucket}
   301                      label={"Bucket"}
   302                      id="bucket"
   303                      name="bucket"
   304                    />
   305                    <FilterInputWrapper
   306                      onChange={setApiName}
   307                      value={apiName}
   308                      label={"API Name"}
   309                      id="api_name"
   310                      name="api_name"
   311                    />
   312                    <FilterInputWrapper
   313                      onChange={setAccessKey}
   314                      value={accessKey}
   315                      label={"Access Key"}
   316                      id="access_key"
   317                      name="access_key"
   318                    />
   319                    <FilterInputWrapper
   320                      onChange={setUserAgent}
   321                      value={userAgent}
   322                      label={"User Agent"}
   323                      id="user_agent"
   324                      name="user_agent"
   325                    />
   326                  </Box>
   327                  <Box sx={filtersContainer}>
   328                    <FilterInputWrapper
   329                      onChange={setObject}
   330                      value={object}
   331                      label={"Object"}
   332                      id="object"
   333                      name="object"
   334                    />
   335                    <FilterInputWrapper
   336                      onChange={setRequestID}
   337                      value={requestID}
   338                      label={"Request ID"}
   339                      id="request_id"
   340                      name="request_id"
   341                    />
   342                    <FilterInputWrapper
   343                      onChange={setResponseStatus}
   344                      value={responseStatus}
   345                      label={"Response Status"}
   346                      id="response_status"
   347                      name="response_status"
   348                    />
   349                  </Box>
   350                </Grid>
   351                <Grid
   352                  item
   353                  xs={12}
   354                  sx={{
   355                    marginBottom: 15,
   356                    padding: "0 15px 0 15px",
   357                    display: "flex",
   358                    alignItems: "center",
   359                    justifyContent: "flex-end",
   360                  }}
   361                >
   362                  <Button
   363                    id={"get-information"}
   364                    type="button"
   365                    variant="callAction"
   366                    onClick={triggerLoad}
   367                    label={"Get Information"}
   368                  />
   369                </Grid>
   370              </Box>
   371              <Grid item xs={12}>
   372                <SecureComponent
   373                  scopes={[IAM_SCOPES.ADMIN_HEALTH_INFO]}
   374                  resource={CONSOLE_UI_RESOURCE}
   375                  errorProps={{ disabled: true }}
   376                >
   377                  <DataTable
   378                    columns={[
   379                      {
   380                        label: LogSearchColumnLabels.time,
   381                        elementKey: "time",
   382                        enableSort: true,
   383                      },
   384                      {
   385                        label: LogSearchColumnLabels.api_name,
   386                        elementKey: "api_name",
   387                      },
   388                      {
   389                        label: LogSearchColumnLabels.access_key,
   390                        elementKey: "access_key",
   391                      },
   392                      {
   393                        label: LogSearchColumnLabels.bucket,
   394                        elementKey: "bucket",
   395                      },
   396                      {
   397                        label: LogSearchColumnLabels.object,
   398                        elementKey: "object",
   399                      },
   400                      {
   401                        label: LogSearchColumnLabels.remote_host,
   402                        elementKey: "remote_host",
   403                      },
   404                      {
   405                        label: LogSearchColumnLabels.request_id,
   406                        elementKey: "request_id",
   407                      },
   408                      {
   409                        label: LogSearchColumnLabels.user_agent,
   410                        elementKey: "user_agent",
   411                      },
   412                      {
   413                        label: LogSearchColumnLabels.response_status,
   414                        elementKey: "response_status",
   415                        renderFunction: (element) => (
   416                          <Fragment>
   417                            <span>
   418                              {element.response_status_code} (
   419                              {element.response_status})
   420                            </span>
   421                          </Fragment>
   422                        ),
   423                        renderFullObject: true,
   424                      },
   425                      {
   426                        label: LogSearchColumnLabels.request_content_length,
   427                        elementKey: "request_content_length",
   428                        renderFunction: niceBytes,
   429                      },
   430                      {
   431                        label: LogSearchColumnLabels.response_content_length,
   432                        elementKey: "response_content_length",
   433                        renderFunction: niceBytes,
   434                      },
   435                      {
   436                        label: LogSearchColumnLabels.time_to_response_ns,
   437                        elementKey: "time_to_response_ns",
   438                        renderFunction: nsToSeconds,
   439                        contentTextAlign: "right",
   440                      },
   441                    ]}
   442                    isLoading={loading}
   443                    records={records}
   444                    entityName="Logs"
   445                    customEmptyMessage={
   446                      "There is no information with this criteria"
   447                    }
   448                    idField="request_id"
   449                    columnsSelector
   450                    columnsShown={columnsShown}
   451                    onColumnChange={selectColumn}
   452                    customPaperHeight={
   453                      filterOpen ? "calc(100vh - 520px)" : "calc(100vh - 320px)"
   454                    }
   455                    sortEnabled={{
   456                      currentSort: "time",
   457                      currentDirection: sortOrder,
   458                      onSortClick: sortChange,
   459                    }}
   460                    infiniteScrollConfig={{
   461                      recordsCount: 1000000,
   462                      loadMoreRecords: loadMoreRecords,
   463                    }}
   464                    itemActions={[
   465                      {
   466                        type: "view",
   467                        onClick: openExtraInformation,
   468                      },
   469                    ]}
   470                    textSelectable
   471                  />
   472                </SecureComponent>
   473              </Grid>
   474            </Fragment>
   475          )}
   476        </PageLayout>
   477      </Fragment>
   478    );
   479  };
   480  
   481  export default LogsSearchMain;