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;