Skip to content

Commit 292fb39

Browse files
authored
Created button to download Widget data as CSV or PNG file (#2241)
1 parent 9ed8f11 commit 292fb39

File tree

7 files changed

+374
-43
lines changed

7 files changed

+374
-43
lines changed

portal-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"moment": "^2.29.4",
3434
"react": "^18.1.0",
3535
"react-chartjs-2": "^2.9.0",
36+
"react-component-export-image": "^1.0.6",
3637
"react-copy-to-clipboard": "^5.0.2",
3738
"react-dom": "^18.1.0",
3839
"react-dropzone": "^11.4.2",
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 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 } from "react";
18+
import { Menu, MenuItem, Box } from "@mui/material";
19+
import ListItemText from "@mui/material/ListItemText";
20+
import { DownloadIcon } from "../../../icons";
21+
import { exportComponentAsPNG } from "react-component-export-image";
22+
import { ErrorResponseHandler } from "../../../common/types";
23+
import { useAppDispatch } from "../../../../src/store";
24+
import { setErrorSnackMessage } from "../../../../src/systemSlice";
25+
interface IDownloadWidgetDataButton {
26+
title: any;
27+
componentRef: any;
28+
data: any;
29+
}
30+
31+
const DownloadWidgetDataButton = ({
32+
title,
33+
componentRef,
34+
data,
35+
}: IDownloadWidgetDataButton) => {
36+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
37+
const openDownloadMenu = Boolean(anchorEl);
38+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
39+
setAnchorEl(event.currentTarget);
40+
};
41+
const handleCloseDownload = () => {
42+
setAnchorEl(null);
43+
};
44+
const download = (filename: string, text: string) => {
45+
let element = document.createElement("a");
46+
element.setAttribute("href", "data:text/plain;charset=utf-8," + text);
47+
element.setAttribute("download", filename);
48+
49+
element.style.display = "none";
50+
document.body.appendChild(element);
51+
52+
element.click();
53+
document.body.removeChild(element);
54+
};
55+
56+
const dispatch = useAppDispatch();
57+
const onDownloadError = (err: ErrorResponseHandler) =>
58+
dispatch(setErrorSnackMessage(err));
59+
60+
const convertToCSV = (objectToConvert: any) => {
61+
const array = [Object.keys(objectToConvert[0])].concat(objectToConvert);
62+
return array
63+
.map((it) => {
64+
return Object.values(it).toString();
65+
})
66+
.join("\n");
67+
};
68+
69+
const widgetDataCSVFileName = () => {
70+
if (title !== null) {
71+
return (title + "_" + Date.now().toString() + ".csv")
72+
.replace(/\s+/g, "")
73+
.trim()
74+
.toLowerCase();
75+
} else {
76+
return "widgetData_" + Date.now().toString() + ".csv";
77+
}
78+
};
79+
80+
const downloadAsCSV = () => {
81+
if (data !== null && data.length > 0) {
82+
download(widgetDataCSVFileName(), convertToCSV(data));
83+
} else {
84+
let err: ErrorResponseHandler;
85+
err = {
86+
errorMessage: "Unable to download widget data",
87+
detailedError: "Unable to download widget data - data not available",
88+
};
89+
onDownloadError(err);
90+
}
91+
};
92+
93+
const downloadAsPNG = () => {
94+
if (title !== null) {
95+
const pngFileName = (title + "_" + Date.now().toString() + ".png")
96+
.replace(/\s+/g, "")
97+
.trim()
98+
.toLowerCase();
99+
exportComponentAsPNG(componentRef, { fileName: pngFileName });
100+
} else {
101+
const pngFileName = "widgetData_" + Date.now().toString() + ".png";
102+
exportComponentAsPNG(componentRef, { fileName: pngFileName });
103+
}
104+
};
105+
106+
return (
107+
<Fragment>
108+
<Box
109+
justifyItems={"center"}
110+
sx={{
111+
"& .download-icon": {
112+
backgroundColor: "transparent",
113+
border: 0,
114+
padding: 0,
115+
cursor: "pointer",
116+
"& svg": {
117+
color: "#D0D0D0",
118+
height: 16,
119+
},
120+
"&:hover": {
121+
"& svg": {
122+
color: "#404143",
123+
},
124+
},
125+
},
126+
}}
127+
>
128+
<button onClick={handleClick} className={"download-icon"}>
129+
<DownloadIcon />
130+
</button>
131+
<Menu
132+
id={`download-widget-main-menu`}
133+
aria-labelledby={`download-widget-main`}
134+
anchorEl={anchorEl}
135+
open={openDownloadMenu}
136+
onClose={() => {
137+
handleCloseDownload();
138+
}}
139+
>
140+
<MenuItem
141+
onClick={() => {
142+
downloadAsCSV();
143+
}}
144+
>
145+
<ListItemText>Download as CSV</ListItemText>
146+
</MenuItem>
147+
<MenuItem
148+
onClick={() => {
149+
downloadAsPNG();
150+
}}
151+
>
152+
<ListItemText>Download as PNG</ListItemText>
153+
</MenuItem>
154+
</Menu>
155+
</Box>
156+
</Fragment>
157+
);
158+
};
159+
160+
export default DownloadWidgetDataButton;

portal-ui/src/screens/Console/Dashboard/Prometheus/PrDashboard.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import React, { Fragment, useCallback, useEffect, useState } from "react";
1818
import { useSelector } from "react-redux";
1919
import Grid from "@mui/material/Grid";
20-
2120
import { Theme } from "@mui/material/styles";
2221
import createStyles from "@mui/styles/createStyles";
2322
import withStyles from "@mui/styles/withStyles";
@@ -146,17 +145,26 @@ const PrDashboard = ({ apiPrefix = "admin" }: IPrDashboard) => {
146145
<Fragment key={`widget-${key}`}>
147146
{panelInfo ? (
148147
<Fragment>
149-
{panelInfo.mergedPanels ? (
150-
<MergedWidgetsRenderer
151-
info={panelInfo}
152-
timeStart={timeStart}
153-
timeEnd={timeEnd}
154-
loading={loading}
155-
apiPrefix={apiPrefix}
156-
/>
157-
) : (
158-
componentToUse(panelInfo, timeStart, timeEnd, loading, apiPrefix)
159-
)}
148+
<Box>
149+
{panelInfo.mergedPanels ? (
150+
<MergedWidgetsRenderer
151+
info={panelInfo}
152+
timeStart={timeStart}
153+
timeEnd={timeEnd}
154+
loading={loading}
155+
apiPrefix={apiPrefix}
156+
/>
157+
) : (
158+
componentToUse(
159+
panelInfo,
160+
timeStart,
161+
timeEnd,
162+
loading,
163+
apiPrefix,
164+
zoomOpen
165+
)
166+
)}
167+
</Box>
160168
</Fragment>
161169
) : null}
162170
</Fragment>

portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/BarChartWidget.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
import React, { Fragment, useEffect, useState } from "react";
17+
import React, { Fragment, useEffect, useState, useRef } from "react";
1818

1919
import {
2020
Bar,
@@ -25,14 +25,13 @@ import {
2525
XAxis,
2626
YAxis,
2727
} from "recharts";
28-
import { useMediaQuery } from "@mui/material";
28+
import { useMediaQuery, Grid } from "@mui/material";
2929
import { Theme } from "@mui/material/styles";
3030
import createStyles from "@mui/styles/createStyles";
3131
import withStyles from "@mui/styles/withStyles";
3232
import { IBarChartConfiguration } from "./types";
3333
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
3434
import BarChartTooltip from "./tooltips/BarChartTooltip";
35-
3635
import { IDashboardPanel } from "../types";
3736
import { widgetDetailsToPanel } from "../utils";
3837
import { ErrorResponseHandler } from "../../../../../common/types";
@@ -42,6 +41,7 @@ import Loader from "../../../Common/Loader/Loader";
4241
import ExpandGraphLink from "./ExpandGraphLink";
4342
import { setErrorSnackMessage } from "../../../../../systemSlice";
4443
import { useAppDispatch } from "../../../../../store";
44+
import DownloadWidgetDataButton from "../../DownloadWidgetDataButton";
4545

4646
interface IBarChartWidget {
4747
classes: any;
@@ -95,6 +95,15 @@ const BarChartWidget = ({
9595
const [loading, setLoading] = useState<boolean>(true);
9696
const [data, setData] = useState<any>([]);
9797
const [result, setResult] = useState<IDashboardPanel | null>(null);
98+
const [hover, setHover] = useState<boolean>(false);
99+
const componentRef = useRef<HTMLElement>();
100+
101+
const onHover = () => {
102+
setHover(true);
103+
};
104+
const onStopHover = () => {
105+
setHover(false);
106+
};
98107

99108
useEffect(() => {
100109
if (propLoading) {
@@ -157,11 +166,27 @@ const BarChartWidget = ({
157166
const biggerThanMd = useMediaQuery(theme.breakpoints.up("md"));
158167

159168
return (
160-
<div className={zoomActivated ? "" : classes.singleValueContainer}>
169+
<div
170+
className={zoomActivated ? "" : classes.singleValueContainer}
171+
onMouseOver={onHover}
172+
onMouseLeave={onStopHover}
173+
>
161174
{!zoomActivated && (
162-
<div className={classes.titleContainer}>
163-
{title} <ExpandGraphLink panelItem={panelItem} />
164-
</div>
175+
<Grid container>
176+
<Grid item xs={10} alignItems={"start"} justifyItems={"start"}>
177+
<div className={classes.titleContainer}>{title}</div>
178+
</Grid>
179+
<Grid item xs={1} display={"flex"} justifyContent={"flex-end"}>
180+
{hover && <ExpandGraphLink panelItem={panelItem} />}
181+
</Grid>
182+
<Grid item xs={1} display={"flex"} justifyContent={"flex-end"}>
183+
<DownloadWidgetDataButton
184+
title={title}
185+
componentRef={componentRef}
186+
data={data}
187+
/>
188+
</Grid>
189+
</Grid>
165190
)}
166191
{loading && (
167192
<div className={classes.loadingAlign}>
@@ -170,6 +195,7 @@ const BarChartWidget = ({
170195
)}
171196
{!loading && (
172197
<div
198+
ref={componentRef as React.RefObject<HTMLDivElement>}
173199
className={
174200
zoomActivated ? classes.zoomChartCont : classes.contentContainer
175201
}

portal-ui/src/screens/Console/Dashboard/Prometheus/Widgets/ExpandGraphLink.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ const ExpandGraphLink = ({ panelItem }: { panelItem: IDashboardPanel }) => {
2727
return (
2828
<Box
2929
sx={{
30-
display: "flex",
31-
alignItems: "center",
30+
alignItems: "right",
3231
gap: "10px",
3332
"& .link-text": {
3433
color: "#2781B0",
@@ -53,17 +52,6 @@ const ExpandGraphLink = ({ panelItem }: { panelItem: IDashboardPanel }) => {
5352
},
5453
}}
5554
>
56-
<a
57-
href={`void:(0);`}
58-
rel="noreferrer noopener"
59-
className={"link-text"}
60-
onClick={(e) => {
61-
e.preventDefault();
62-
dispatch(openZoomPage(panelItem));
63-
}}
64-
>
65-
Expand Graph
66-
</a>
6755
<button
6856
onClick={() => {
6957
dispatch(openZoomPage(panelItem));

0 commit comments

Comments
 (0)