github.com/minio/console@v1.4.1/web-app/src/screens/Console/Dashboard/Prometheus/Widgets/LinearGraphWidget.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, useRef, useState } from "react";
    18  import styled from "styled-components";
    19  import get from "lodash/get";
    20  import { useSelector } from "react-redux";
    21  import {
    22    Area,
    23    AreaChart,
    24    CartesianGrid,
    25    ResponsiveContainer,
    26    Tooltip,
    27    XAxis,
    28    YAxis,
    29  } from "recharts";
    30  import { Box, breakPoints, Grid, Loader } from "mds";
    31  import { ILinearGraphConfiguration } from "./types";
    32  import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
    33  import { IDashboardPanel } from "../types";
    34  import { widgetDetailsToPanel } from "../utils";
    35  import { ErrorResponseHandler } from "../../../../../common/types";
    36  import { setErrorSnackMessage } from "../../../../../systemSlice";
    37  import { AppState, useAppDispatch } from "../../../../../store";
    38  import api from "../../../../../common/api";
    39  import LineChartTooltip from "./tooltips/LineChartTooltip";
    40  import ExpandGraphLink from "./ExpandGraphLink";
    41  import DownloadWidgetDataButton from "../../DownloadWidgetDataButton";
    42  
    43  interface ILinearGraphWidget {
    44    title: string;
    45    panelItem: IDashboardPanel;
    46    timeStart: any;
    47    timeEnd: any;
    48    apiPrefix: string;
    49    hideYAxis?: boolean;
    50    yAxisFormatter?: (item: string) => string;
    51    xAxisFormatter?: (item: string, var1: boolean, var2: boolean) => string;
    52    areaWidget?: boolean;
    53    zoomActivated?: boolean;
    54  }
    55  
    56  const LinearGraphMain = styled.div(({ theme }) => ({
    57    ...widgetCommon(theme),
    58    "& .chartCont": {
    59      position: "relative",
    60      height: 140,
    61      width: "100%",
    62    },
    63    "& .legendChart": {
    64      display: "flex",
    65      flexDirection: "column",
    66      flex: "0 1 auto",
    67      maxHeight: 130,
    68      margin: 0,
    69      overflowY: "auto",
    70      position: "relative",
    71      textAlign: "center",
    72      width: "100%",
    73      justifyContent: "flex-start",
    74      color: get(theme, "mutedText", "#87888d"),
    75      fontWeight: "bold",
    76      fontSize: 12,
    77      [`@media (max-width: ${breakPoints.md}px)`]: {
    78        display: "none",
    79      },
    80    },
    81    "& .loadingAlign": {
    82      width: 40,
    83      height: 40,
    84      textAlign: "center",
    85      margin: "15px auto",
    86    },
    87  }));
    88  
    89  const LinearGraphWidget = ({
    90    title,
    91    timeStart,
    92    timeEnd,
    93    panelItem,
    94    apiPrefix,
    95    hideYAxis = false,
    96    areaWidget = false,
    97    yAxisFormatter = (item: string) => item,
    98    xAxisFormatter = (item: string, var1: boolean, var2: boolean) => item,
    99    zoomActivated = false,
   100  }: ILinearGraphWidget) => {
   101    const dispatch = useAppDispatch();
   102    const [loading, setLoading] = useState<boolean>(false);
   103    const [hover, setHover] = useState<boolean>(false);
   104    const [data, setData] = useState<object[]>([]);
   105    const [csvData, setCsvData] = useState<object[]>([]);
   106    const [dataMax, setDataMax] = useState<number>(0);
   107    const [result, setResult] = useState<IDashboardPanel | null>(null);
   108    const widgetVersion = useSelector(
   109      (state: AppState) => state.dashboard.widgetLoadVersion,
   110    );
   111  
   112    const componentRef = useRef(null);
   113  
   114    useEffect(() => {
   115      setLoading(true);
   116    }, [widgetVersion]);
   117  
   118    useEffect(() => {
   119      if (loading) {
   120        let stepCalc = 0;
   121        if (timeStart !== null && timeEnd !== null) {
   122          const secondsInPeriod =
   123            timeEnd.toUnixInteger() - timeStart.toUnixInteger();
   124          const periods = Math.floor(secondsInPeriod / 60);
   125  
   126          stepCalc = periods < 1 ? 15 : periods;
   127        }
   128  
   129        api
   130          .invoke(
   131            "GET",
   132            `/api/v1/${apiPrefix}/info/widgets/${
   133              panelItem.id
   134            }/?step=${stepCalc}&${
   135              timeStart !== null ? `&start=${timeStart.toUnixInteger()}` : ""
   136            }${timeStart !== null && timeEnd !== null ? "&" : ""}${
   137              timeEnd !== null ? `end=${timeEnd.toUnixInteger()}` : ""
   138            }`,
   139          )
   140          .then((res: any) => {
   141            const widgetsWithValue = widgetDetailsToPanel(res, panelItem);
   142            setData(widgetsWithValue.data);
   143            setResult(widgetsWithValue);
   144            setLoading(false);
   145            let maxVal = 0;
   146            for (const dp of widgetsWithValue.data) {
   147              for (const key in dp) {
   148                if (key === "name") {
   149                  continue;
   150                }
   151                let val = parseInt(dp[key]);
   152  
   153                if (isNaN(val)) {
   154                  val = 0;
   155                }
   156  
   157                if (maxVal < val) {
   158                  maxVal = val;
   159                }
   160              }
   161            }
   162            setDataMax(maxVal);
   163          })
   164          .catch((err: ErrorResponseHandler) => {
   165            dispatch(setErrorSnackMessage(err));
   166            setLoading(false);
   167          });
   168      }
   169    }, [loading, panelItem, timeEnd, timeStart, dispatch, apiPrefix]);
   170  
   171    let intervalCount = Math.floor(data.length / 5);
   172  
   173    const onHover = () => {
   174      setHover(true);
   175    };
   176  
   177    const onStopHover = () => {
   178      setHover(false);
   179    };
   180  
   181    useEffect(() => {
   182      const fmtData = data.map((el: any) => {
   183        const date = new Date(el?.name * 1000);
   184        return {
   185          ...el,
   186          name: date,
   187        };
   188      });
   189  
   190      setCsvData(fmtData);
   191    }, [data]);
   192  
   193    const linearConfiguration = result
   194      ? (result?.widgetConfiguration as ILinearGraphConfiguration[])
   195      : [];
   196  
   197    const CustomizedDot = (prop: any) => {
   198      const { cx, cy, index } = prop;
   199  
   200      if (index % 3 !== 0) {
   201        return null;
   202      }
   203      return <circle cx={cx} cy={cy} r={3} strokeWidth={0} fill="#07264A" />;
   204    };
   205  
   206    let dspLongDate = false;
   207  
   208    if (zoomActivated) {
   209      dspLongDate = true;
   210    }
   211  
   212    return (
   213      <LinearGraphMain>
   214        <Box
   215          className={zoomActivated ? "" : "singleValueContainer"}
   216          onMouseOver={onHover}
   217          onMouseLeave={onStopHover}
   218        >
   219          {!zoomActivated && (
   220            <Grid container>
   221              <Grid item xs={10} sx={{ alignItems: "start" }}>
   222                <Box className={"titleContainer"}>{title}</Box>
   223              </Grid>
   224              <Grid
   225                item
   226                xs={1}
   227                sx={{
   228                  display: "flex",
   229                  justifyContent: "flex-end",
   230                  alignContent: "flex-end",
   231                }}
   232              >
   233                {hover && <ExpandGraphLink panelItem={panelItem} />}
   234              </Grid>
   235              <Grid
   236                item
   237                xs={1}
   238                sx={{ display: "flex", justifyContent: "flex-end" }}
   239              >
   240                {componentRef !== null && (
   241                  <DownloadWidgetDataButton
   242                    title={title}
   243                    componentRef={componentRef}
   244                    data={csvData}
   245                  />
   246                )}
   247              </Grid>
   248            </Grid>
   249          )}
   250          <div ref={componentRef}>
   251            <Box
   252              sx={
   253                zoomActivated
   254                  ? { flexDirection: "column" }
   255                  : {
   256                      height: "100%",
   257                      display: "grid",
   258                      gridTemplateColumns: "1fr 1fr",
   259                      [`@media (max-width: ${breakPoints.md}px)`]: {
   260                        gridTemplateColumns: "1fr",
   261                      },
   262                    }
   263              }
   264              style={areaWidget ? { gridTemplateColumns: "1fr" } : {}}
   265            >
   266              {loading && <Loader className={"loadingAlign"} />}
   267              {!loading && (
   268                <Fragment>
   269                  <Box className={zoomActivated ? "zoomChartCont" : "chartCont"}>
   270                    <ResponsiveContainer width="99%">
   271                      <AreaChart
   272                        data={data}
   273                        margin={{
   274                          top: 5,
   275                          right: 20,
   276                          left: hideYAxis ? 20 : 5,
   277                          bottom: 0,
   278                        }}
   279                      >
   280                        {areaWidget && (
   281                          <defs>
   282                            <linearGradient
   283                              id="colorUv"
   284                              x1="0"
   285                              y1="0"
   286                              x2="0"
   287                              y2="1"
   288                            >
   289                              <stop
   290                                offset="0%"
   291                                stopColor="#2781B0"
   292                                stopOpacity={1}
   293                              />
   294                              <stop
   295                                offset="100%"
   296                                stopColor="#ffffff"
   297                                stopOpacity={0}
   298                              />
   299  
   300                              <stop
   301                                offset="95%"
   302                                stopColor="#ffffff"
   303                                stopOpacity={0.8}
   304                              />
   305                            </linearGradient>
   306                          </defs>
   307                        )}
   308                        <CartesianGrid
   309                          strokeDasharray={areaWidget ? "2 2" : "5 5"}
   310                          strokeWidth={1}
   311                          strokeOpacity={1}
   312                          stroke={"#eee0e0"}
   313                          vertical={!areaWidget}
   314                        />
   315                        <XAxis
   316                          dataKey="name"
   317                          tickFormatter={(value: any) =>
   318                            xAxisFormatter(value, dspLongDate, true)
   319                          }
   320                          interval={intervalCount}
   321                          tick={{
   322                            fontSize: "68%",
   323                            fontWeight: "normal",
   324                            color: "#404143",
   325                          }}
   326                          tickCount={10}
   327                          stroke={"#082045"}
   328                        />
   329                        <YAxis
   330                          type={"number"}
   331                          domain={[0, dataMax * 1.1]}
   332                          hide={hideYAxis}
   333                          tickFormatter={(value: any) => yAxisFormatter(value)}
   334                          tick={{
   335                            fontSize: "68%",
   336                            fontWeight: "normal",
   337                            color: "#404143",
   338                          }}
   339                          stroke={"#082045"}
   340                        />
   341                        {linearConfiguration.map((section, index) => {
   342                          return (
   343                            <Area
   344                              key={`area-${section.dataKey}-${index.toString()}`}
   345                              type="monotone"
   346                              dataKey={section.dataKey}
   347                              isAnimationActive={false}
   348                              stroke={!areaWidget ? section.lineColor : "#D7E5F8"}
   349                              fill={
   350                                areaWidget ? "url(#colorUv)" : section.fillColor
   351                              }
   352                              fillOpacity={areaWidget ? 0.65 : 0}
   353                              strokeWidth={!areaWidget ? 3 : 0}
   354                              strokeLinecap={"round"}
   355                              dot={areaWidget ? <CustomizedDot /> : false}
   356                            />
   357                          );
   358                        })}
   359                        <Tooltip
   360                          content={
   361                            <LineChartTooltip
   362                              linearConfiguration={linearConfiguration}
   363                              yAxisFormatter={yAxisFormatter}
   364                            />
   365                          }
   366                          wrapperStyle={{
   367                            zIndex: 5000,
   368                          }}
   369                        />
   370                      </AreaChart>
   371                    </ResponsiveContainer>
   372                  </Box>
   373                  {!areaWidget && (
   374                    <Fragment>
   375                      {zoomActivated && (
   376                        <Fragment>
   377                          <strong>Series</strong>
   378                          <br />
   379                          <br />
   380                        </Fragment>
   381                      )}
   382  
   383                      <Box className={"legendChart"}>
   384                        {linearConfiguration.map((section, index) => {
   385                          return (
   386                            <Box
   387                              className={"singleLegendContainer"}
   388                              key={`legend-${
   389                                section.keyLabel
   390                              }-${index.toString()}`}
   391                            >
   392                              <Box
   393                                className={"colorContainer"}
   394                                style={{ backgroundColor: section.lineColor }}
   395                              />
   396                              <Box className={"legendLabel"}>
   397                                {section.keyLabel}
   398                              </Box>
   399                            </Box>
   400                          );
   401                        })}
   402                      </Box>
   403                    </Fragment>
   404                  )}
   405                </Fragment>
   406              )}
   407            </Box>
   408          </div>
   409        </Box>
   410      </LinearGraphMain>
   411    );
   412  };
   413  
   414  export default LinearGraphWidget;