UX Basic Dashboard (#1513)

This commit is contained in:
Prakash Senthil Vel
2022-02-04 05:34:13 +00:00
committed by GitHub
parent 608a5c3787
commit 1c31aff147
13 changed files with 1253 additions and 458 deletions

View File

@@ -0,0 +1,69 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const PrometheusErrorIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 23.786 22.2"
{...props}
>
<defs>
<clipPath id="clip-path-prom-error">
<rect
id="Rectángulo_1578"
data-name="Rectángulo 1578"
width="23.786"
height="22.2"
fill="none"
/>
</clipPath>
</defs>
<g
id="Grupo_2402"
data-name="Grupo 2402"
clipPath="url(#clip-path-prom-error)"
>
<path
id="Trazado_7049"
data-name="Trazado 7049"
d="M23.786,7.136a3.967,3.967,0,0,0-4.824-3.871A11.1,11.1,0,1,0,22.2,11.1c0-.26-.01-.518-.027-.773a3.958,3.958,0,0,0,1.613-3.192M11.1,20.776v0a2.92,2.92,0,0,1-3.158-2.6h6.317a2.922,2.922,0,0,1-3.159,2.6m5.217-3.464H5.883V15.42H16.317Zm-.038-2.865H5.913c-.035-.04-.07-.079-.1-.119a7.561,7.561,0,0,1-1.564-2.664c0-.023,1.295.266,2.22.476,0,0,.476.109,1.167.238A4.332,4.332,0,0,1,6.573,9.592c0-2.225,1.707-4.17,1.091-5.741.6.048,1.24,1.269,1.284,3.166a6.8,6.8,0,0,0,.9-3.474c0-1.02.672-2.207,1.348-2.247-.6.988.159,1.835.826,3.937.251.793.22,2.118.414,2.961.064-1.75.366-4.3,1.476-5.185a3.83,3.83,0,0,0,.457,3.167,6,6,0,0,1,1,3.437,4.294,4.294,0,0,1-1.031,2.775c.733-.137,1.239-.262,1.239-.262l2.379-.465a6.749,6.749,0,0,1-1.676,2.785M19.822,10.7A3.568,3.568,0,1,1,23.39,7.136,3.568,3.568,0,0,1,19.822,10.7"
transform="translate(0 -0.001)"
fill="#c83b51"
/>
<path
id="Trazado_7050"
data-name="Trazado 7050"
d="M491.022,131.222l.121-2.851h-1.17l.121,2.851Z"
transform="translate(-470.607 -123.297)"
fill="#c83b51"
/>
<path
id="Trazado_7051"
data-name="Trazado 7051"
d="M488.865,209.66a.655.655,0,1,0,.65.65.667.667,0,0,0-.65-.65"
transform="translate(-468.913 -201.374)"
fill="#c83b51"
/>
</g>
</svg>
);
export default PrometheusErrorIcon;

View File

@@ -162,3 +162,4 @@ export { default as HelpIconFilled } from "./HelpIconFilled";
export { default as CallHomeFeatureIcon } from "./CallHomeFeatureIcon";
export { default as DiagnosticsFeatureIcon } from "./DiagnosticsFeatureIcon";
export { default as PerformanceFeatureIcon } from "./PerformanceFeatureIcon";
export { default as PrometheusErrorIcon } from "./PrometheusErrorIcon";

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,95 +14,86 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import { IDriveInfo, Usage } from "../types";
import { calculateBytes, representationNumber } from "../../../../common/utils";
import { TabPanel } from "../../../shared/tabs";
import ServerInfoCard from "./ServerInfoCard";
import DriveInfoCard from "./DriveInfoCard";
import CommonCard from "../CommonCard";
import TabSelector from "../../Common/TabSelector/TabSelector";
import GeneralUsePaginator from "../../Common/GeneralUsePaginator/GeneralUsePaginator";
import { widgetContainerCommon } from "../../Common/FormComponents/common/styleLibrary";
import { PrometheusIcon } from "../../../../icons";
import React, { Fragment } from "react";
import { Box } from "@mui/material";
import {
BucketsIcon,
DrivesIcon,
PrometheusErrorIcon,
ServersIcon,
TotalObjectsIcon,
} from "../../../../icons";
import HelpBox from "../../../../common/HelpBox";
import { calculateBytes, representationNumber } from "../../../../common/utils";
import { IDriveInfo, Usage } from "../types";
import StatusCountCard from "./StatusCountCard";
import groupBy from "lodash/groupBy";
import ServersList from "./ServersList";
import CounterCard from "./CounterCard";
import ReportedUsage from "./ReportedUsage";
const styles = (theme: Theme) =>
createStyles({
generalStatusTitle: {
color: "#767676",
fontSize: 16,
fontWeight: "bold",
margin: "15px 10px 0 10px",
},
paginatorContainer: {
maxWidth: 1185,
width: "100%",
},
...widgetContainerCommon,
});
const BoxItem = ({
children,
background = "#ffffff",
}: {
children: any;
background?: string;
}) => {
return (
<Box
sx={{
border: "1px solid #f1f1f1",
background: background,
maxWidth: {
sm: "100%",
xs: "250px",
},
}}
>
{children}
</Box>
);
};
interface IDashboardProps {
classes: any;
usage: Usage | null;
}
const itemsPerPage = 5;
const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
const [curTab, setCurTab] = useState<number>(0);
const [serversPageNumber, setServersPageNumber] = useState<number>(1);
const [drivesPageNumber, setDrivesPageNumber] = useState<number>(1);
const prettyUsage = (usage: string | undefined) => {
if (usage === undefined) {
return { total: "0", unit: "Mi" };
}
const calculatedBytes = calculateBytes(usage);
return calculatedBytes;
};
const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
const getServersList = (usage: Usage | null) => {
if (usage !== null) {
return usage.servers.sort(function (a, b) {
const nameA = a.endpoint.toLowerCase();
const nameB = b.endpoint.toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
}
});
}
return usage.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
return [];
};
const makeServerArray = (usage: Usage | null) => {
if (usage !== null) {
return usage.servers.sort(function (a, b) {
var nameA = a.endpoint.toLowerCase();
var nameB = b.endpoint.toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
}
const prettyUsage = (usage: string | undefined) => {
if (usage === undefined) {
return { total: "0", unit: "Mi" };
}
return [];
};
return calculateBytes(usage);
};
const serverArray = makeServerArray(usage || null);
const BasicDashboard = ({ usage }: IDashboardProps) => {
const usageValue = usage && usage.usage ? usage.usage.toString() : "0";
const usageToRepresent = prettyUsage(usageValue);
const usageToRepresent = prettyUsage(
usage && usage.usage ? usage.usage.toString() : "0"
);
const serverList = getServersList(usage || null);
let allDrivesArray: IDriveInfo[] = [];
serverArray.forEach((server) => {
serverList.forEach((server) => {
const drivesInput = server.drives.map((drive) => {
return drive;
});
@@ -110,29 +101,35 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
allDrivesArray = [...allDrivesArray, ...drivesInput];
});
const splitedServers = serverArray.slice(
serversPageNumber * itemsPerPage - itemsPerPage,
serversPageNumber * itemsPerPage
);
const splitedDrives = allDrivesArray.slice(
drivesPageNumber * itemsPerPage - itemsPerPage,
drivesPageNumber * itemsPerPage
);
const serversGroup = groupBy(serverList, "state");
const { offline: offlineServers = [], online: onlineServers = [] } =
serversGroup;
const drivesGroup = groupBy(allDrivesArray, "state");
const { offline: offlineDrives = [], ok: onlineDrives = [] } = drivesGroup;
return (
<Fragment>
<div className={classes.dashboardBG} />
{usage?.prometheusNotReady && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<Box
sx={{
maxWidth: "1536px",
margin: "auto",
}}
>
<Box
sx={{
display: "grid",
gridTemplateRows: "1fr",
gridTemplateColumns: "1fr",
gap: "27px",
marginBottom: "40px",
marginTop: "80px",
marginLeft: "60px",
marginRight: "60px",
}}
>
<Box>
{usage?.prometheusNotReady && (
<HelpBox
iconComponent={<PrometheusIcon />}
iconComponent={<PrometheusErrorIcon />}
title={"We can't retrieve advanced metrics at this time"}
help={
<Fragment>
@@ -145,160 +142,131 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
</Fragment>
}
/>
</Grid>
</Grid>
)}
<Grid container spacing={2}>
<Grid item xs={12} className={classes.generalStatusTitle}>
General Status
</Grid>
<Grid item xs={12} className={classes.dashboardRow}>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"All Buckets"}
metricValue={usage ? representationNumber(usage.buckets) : 0}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Usage"}
metricValue={usageToRepresent.total}
metricUnit={usageToRepresent.unit}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Total Objects"}
metricValue={usage ? representationNumber(usage.objects) : 0}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Servers"}
metricValue={usage ? prettyNumber(serverArray.length) : 0}
subMessage={{ message: "Total" }}
extraMargin
/>
</Grid>
</Grid>
<Grid item xs={12}>
<TabSelector
selectedTab={curTab}
onChange={(newValue: number) => {
setCurTab(newValue);
}}
tabOptions={[{ label: "Servers" }, { label: "Drives" }]}
/>
</Grid>
<Grid item xs={12} className={classes.widgetsContainer}>
<TabPanel index={0} value={curTab}>
<div className={classes.paginatorContainer}>
<GeneralUsePaginator
page={serversPageNumber}
entity={"Servers"}
totalItems={serverArray.length}
onChange={setServersPageNumber}
itemsPerPage={itemsPerPage}
/>
</div>
{splitedServers.map((server, index) => (
<Grid item xs={12} key={`serverDS-${index.toString()}`}>
<ServerInfoCard server={server} index={index + 1} />
</Grid>
))}
</TabPanel>
<TabPanel index={1} value={curTab}>
<div className={classes.paginatorContainer}>
<GeneralUsePaginator
page={drivesPageNumber}
entity={"Drives"}
totalItems={allDrivesArray.length}
onChange={setDrivesPageNumber}
itemsPerPage={itemsPerPage}
/>
</div>
{splitedDrives.map((drive, index) => (
<Grid item xs={12} key={`drive-${index}-${drive.uuid}`}>
<DriveInfoCard drive={drive} />
</Grid>
))}
</TabPanel>
</Grid>
</Grid>
{!usage?.prometheusNotReady && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
)}
{!usage?.prometheusNotReady && (
<HelpBox
iconComponent={<PrometheusIcon />}
title={"Monitoring"}
iconComponent={<PrometheusErrorIcon />}
title={"We cant retrieve advanced metrics at this time."}
help={
<Fragment>
The MinIO Dashboard is displaying basic metrics only due to
missing the{" "}
<a
href="https://docs.min.io/minio/baremetal/console/minio-console.html?ref=con#configuration"
target="_blank"
rel="noreferrer"
<Box>
<Box
sx={{
fontSize: "14px",
}}
>
necessary settings
</a>{" "}
for displaying extended metrics.
<br />
<br />
See{" "}
<a
href="https://docs.min.io/minio/baremetal/monitoring/metrics-alerts/collect-minio-metrics-using-prometheus.html?ref=con#minio-metrics-collect-using-prometheus"
target="_blank"
rel="noreferrer"
MinIO Dashboard will display basic metrics as we couldnt
connect to Prometheus successfully. Please try again in a
few minutes. If the problem persists, you can review your
configuration and confirm that Prometheus server is up and
running.
</Box>
<Box
sx={{
paddingTop: "20px",
fontSize: "14px",
"& a": {
color: (theme) => theme.colors.link,
},
}}
>
Collect MinIO Metrics Using Prometheus
</a>{" "}
for a complete tutorial on scraping and visualizing MinIO
metrics with Prometheus.
</Fragment>
<a
href="https://docs.min.io/minio/baremetal/monitoring/metrics-alerts/collect-minio-metrics-using-prometheus.html?ref=con#minio-metrics-collect-using-prometheus"
target="_blank"
rel="noreferrer"
>
Read more about Prometheus on our Docs site.
</a>
</Box>
</Box>
}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
</Box>
<Box
sx={{
display: "grid",
gridTemplateRows: "1fr .2fr auto",
gridTemplateColumns: "1fr",
gap: "40px",
}}
>
<Box
sx={{
display: "grid",
gridTemplateRows: "1fr",
gridTemplateColumns: {
lg: "1fr 1fr 1fr 1fr ",
sm: "1fr 1fr",
xs: "1fr",
},
gap: "40px",
}}
>
<BoxItem
background={
"linear-gradient(-15deg, #2781b0 0%, #ffffff 30%) 0% 0% no-repeat padding-box"
}
>
<CounterCard
label={"Buckets"}
icon={<BucketsIcon />}
counterValue={usage ? representationNumber(usage.buckets) : 0}
/>
</BoxItem>
<BoxItem
background={
"linear-gradient(-15deg, #4CCB92 0%, #ffffff 30%) 0% 0% no-repeat padding-box"
}
>
<CounterCard
label={"Objects"}
icon={<TotalObjectsIcon />}
counterValue={usage ? representationNumber(usage.objects) : 0}
/>
</BoxItem>
<BoxItem>
<StatusCountCard
onlineCount={onlineServers.length}
offlineCount={offlineServers.length}
label={"Servers"}
icon={<ServersIcon />}
/>
</BoxItem>
<BoxItem>
<StatusCountCard
offlineCount={offlineDrives.length}
onlineCount={onlineDrives.length}
label={"Drives"}
icon={<DrivesIcon />}
/>
</BoxItem>
</Box>
<BoxItem>
<ReportedUsage
usageValue={usageValue}
total={usageToRepresent.total}
unit={usageToRepresent.unit}
/>
</BoxItem>
<Box
sx={{
display: "grid",
gridTemplateRows: "auto",
gridTemplateColumns: "1fr",
gap: "auto",
}}
>
<BoxItem>
<ServersList data={serverList} />
</BoxItem>
</Box>
</Box>
</Box>
</Box>
);
};
export default withStyles(styles)(BasicDashboard);
export default BasicDashboard;

View File

@@ -0,0 +1,118 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Box, Tooltip } from "@mui/material";
import React from "react";
const CounterCard = ({
counterValue,
label = "",
icon = null,
}: {
counterValue: string | number;
label?: any;
icon?: any;
}) => {
return (
<Box
sx={{
fontFamily: "Lato,sans-serif",
color: "#07193E",
maxWidth: "300px",
minHeight: "200px",
display: "flex",
marginLeft: "auto",
marginRight: "auto",
cursor: "default",
position: "relative",
width: "100%",
}}
>
<Box
sx={{
flex: 1,
height: "200px",
display: "flex",
width: "100%",
padding: {
sm: "0 8px 0 8px",
xs: "0 10px 0 10px",
},
position: "absolute",
}}
>
<Box
sx={{
flex: 1,
display: "flex",
flexFlow: "column",
marginTop: "32px",
zIndex: 10,
overflow: "hidden",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
}}
>
{label}
</Box>
<Tooltip title={counterValue} placement="bottom" enterDelay={500}>
<Box
sx={{
fontSize: {
xl: "55px",
lg: "40px",
md: "36px",
sm: "22px",
xs: "14px",
},
fontWeight: 600,
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: {
md: 187,
xs: 200,
},
}}
>
{counterValue}
</Box>
</Tooltip>
</Box>
<Box
sx={{
width: "20px",
height: "20px",
marginTop: "26px",
maxWidth: "26px",
"& .min-icon": {
width: "16px",
height: "16px",
},
}}
>
{icon}
</Box>
</Box>
</Box>
);
};
export default CounterCard;

View File

@@ -1,91 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import { IDriveInfo } from "../types";
import { niceBytes } from "../../../../common/utils";
import { Card, CardHeader } from "@mui/material";
import { CircleIcon } from "../../../../icons";
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...commonDashboardInfocard,
});
interface ICardProps {
classes: any;
drive: IDriveInfo;
}
const DriveInfoCard = ({ classes, drive }: ICardProps) => {
const driveStatusToClass = (health_status: string) => {
switch (health_status) {
case "offline":
return classes.redState;
case "ok":
return classes.greenState;
default:
return classes.greyState;
}
};
return (
<Fragment>
<Card className={classes.cardContainer}>
<CardHeader
className={classes.cardHeader}
title={
<div className={classes.referenceTitle}>
{drive.state && (
<span className={driveStatusToClass(drive.state)}>
<CircleIcon />
</span>
)}
{drive.endpoint || ""}
</div>
}
subheader={
<Grid item xs={12} className={classes.stateContainer}>
<span className={classes.infoValue}>
<strong>Capacity:</strong>{" "}
{niceBytes(
drive.totalSpace ? drive.totalSpace.toString() : "0"
)}
</span>
<span className={classes.infoValue}>
<strong>Used:</strong>{" "}
{niceBytes(drive.usedSpace ? drive.usedSpace.toString() : "0")}
</span>
<span className={classes.infoValue}>
<strong>Available:</strong>{" "}
{niceBytes(
drive.availableSpace ? drive.availableSpace.toString() : "0"
)}
</span>
</Grid>
}
/>
</Card>
</Fragment>
);
};
export default withStyles(styles)(DriveInfoCard);

View File

@@ -0,0 +1,175 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IDriveInfo } from "../types";
import { niceBytes } from "../../../../common/utils";
import { Box } from "@mui/material";
import { CircleIcon, DrivesIcon } from "../../../../icons";
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
import { STATUS_COLORS } from "./Utils";
const styles = (theme: Theme) =>
createStyles({
...commonDashboardInfocard,
});
interface ICardProps {
classes?: any;
drive: IDriveInfo;
}
const driveStatusColor = (health_status: string) => {
switch (health_status) {
case "offline":
return STATUS_COLORS.RED;
case "ok":
return STATUS_COLORS.GREEN;
default:
return STATUS_COLORS.YELLOW;
}
};
const DriveInfoItem = ({ classes, drive }: ICardProps) => {
return (
<Box
sx={{
display: "flex",
flex: 1,
alignItems: "center",
paddingBottom: "10px",
borderBottom: {
xs: "1px solid #eaeaea",
},
}}
>
<Box
sx={{
"& .min-icon": {
fill: "#848484",
},
}}
>
<DrivesIcon />
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
marginLeft: "10px",
flex: 1,
}}
>
<Box
sx={{
fontSize: "14px",
fontWeight: 400,
display: "flex",
alignItems: "center",
"& .min-icon": {
marginRight: "10px",
height: "10px",
width: "10px",
fill: driveStatusColor(drive.state),
flexShrink: 0,
},
"& .drive-endpoint": {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "normal",
wordBreak: "break-all",
fontSize: {
md: "14px",
xs: "10px",
},
},
}}
>
{drive.state && <CircleIcon />}
<div className="drive-endpoint">{drive.endpoint || ""}</div>
</Box>
<Box
sx={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
paddingLeft: "20px",
marginTop: "10px",
flexFlow: {
sm: "row",
xs: "column",
},
"& .info-label": {
color: "#8399AB",
},
"& .info-value": {
color: "#073052",
fontSize: "14px",
fontWeight: 500,
},
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<label className="info-label">Capacity:</label>
<div className="info-value">
{niceBytes(drive.totalSpace ? drive.totalSpace.toString() : "0")}
</div>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<label className="info-label">Used:</label>
<div className="info-value">
{niceBytes(drive.usedSpace ? drive.usedSpace.toString() : "0")}
</div>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<label className="info-label">Available:</label>
<div className="info-value">
{niceBytes(
drive.availableSpace ? drive.availableSpace.toString() : "0"
)}
</div>
</Box>
</Box>
</Box>
</Box>
);
};
export default withStyles(styles)(DriveInfoItem);

View File

@@ -0,0 +1,85 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ReportedUsageIcon } from "../../../../icons";
import { Box, Tooltip } from "@mui/material";
import React from "react";
const ReportedUsage = ({
usageValue,
total,
unit,
}: {
usageValue: string;
total: number | string;
unit: string;
}) => {
return (
<Box
sx={{
maxHeight: "110px",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "19px",
padding: "10px",
"& .unit-value": {
fontSize: "50px",
color: "#07193E",
},
"& .unit-type": {
fontSize: "18px",
color: "#5E5E5E",
marginTop: "20px",
marginLeft: "5px",
},
"& .usage-label": {
display: "flex",
alignItems: "center",
fontSize: "16px",
fontWeight: 600,
marginRight: "20px",
marginTop: "-10px",
"& .min-icon": {
marginLeft: "10px",
height: 16,
width: 16,
},
},
}}
>
<div className="usage-label">
<span>Reported Usage</span> <ReportedUsageIcon />
</div>
<Tooltip title={`${usageValue} Bytes`}>
<label
className={"unit-value"}
style={{
fontWeight: 600,
}}
>
{total}
</label>
</Tooltip>
<label className={"unit-type"}>{unit}</label>
</Box>
);
};
export default ReportedUsage;

View File

@@ -1,121 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import { ServerInfo } from "../types";
import { niceDays } from "../../../../common/utils";
import { Card, CardHeader } from "@mui/material";
import { CircleIcon, VersionIcon } from "../../../../icons";
import get from "lodash/get";
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...commonDashboardInfocard,
});
interface ICardProps {
classes: any;
server: ServerInfo;
index: number;
}
const ServerInfoCard = ({ classes, server, index }: ICardProps) => {
const serverStatusToClass = (health_status: string) => {
switch (health_status) {
case "offline":
return classes.redState;
case "online":
return classes.greenState;
default:
return classes.greyState;
}
};
const networkKeys = Object.keys(get(server, "network", {}));
const networkTotal = networkKeys.length;
const totalDrives = server.drives ? server.drives.length : 0;
const activeNetwork = networkKeys.reduce((acc: number, currValue: string) => {
const item = server.network[currValue];
if (item === "online") {
return acc + 1;
}
return acc;
}, 0);
const activeDisks = server.drives
? server.drives.filter((element) => element.state === "ok").length
: 0;
return (
<Card className={classes.cardContainer}>
<CardHeader
className={classes.cardHeader}
title={
<div>
<div className={classes.cardNumber}>Server {index}</div>
<div className={classes.referenceTitle}>
{server.state && (
<span className={serverStatusToClass(server.state)}>
<CircleIcon />
</span>
)}
{server.endpoint || ""}
</div>
</div>
}
subheader={
<Grid item xs={12} className={classes.stateContainer}>
<span className={classes.infoValue}>
<span
className={`${classes.innerState} ${
activeDisks <= totalDrives / 2 && classes.redState
} ${
totalDrives !== 2 &&
activeDisks === totalDrives / 2 + 1 &&
classes.yellowState
} ${activeDisks === totalDrives && classes.greenState}`}
>
<CircleIcon />
</span>
Drives: {activeDisks}/{totalDrives}{" "}
</span>
<span className={classes.infoValue}>
<span
className={`${classes.innerState} ${
activeNetwork <= networkTotal / 2 && classes.redState
} ${
activeNetwork === networkTotal / 2 + 1 && classes.yellowState
} ${activeNetwork === networkTotal && classes.greenState}`}
>
<CircleIcon />
</span>
Network: {activeNetwork}/{networkTotal}{" "}
</span>
<span className={classes.infoValue}>
Uptime: {server.uptime ? niceDays(server.uptime) : "N/A"}
</span>
<span className={classes.infoValue}>
<VersionIcon />
<strong>Version</strong> {server.version ? server.version : "N/A"}
</span>
</Grid>
}
/>
</Card>
);
};
export default withStyles(styles)(ServerInfoCard);

View File

@@ -0,0 +1,239 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { ServerInfo } from "../types";
import { niceDays } from "../../../../common/utils";
import { Box } from "@mui/material";
import {
CircleIcon,
DrivesIcon,
UptimeIcon,
VersionIcon,
WarpIcon,
} from "../../../../icons";
import get from "lodash/get";
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
import {
getDriveStatusColor,
getNetworkStatusColor,
serverStatusColor,
} from "./Utils";
const styles = (theme: Theme) =>
createStyles({
...commonDashboardInfocard,
});
interface ICardProps {
classes?: any;
server: ServerInfo;
index: number;
}
const ServerStatItem = ({
label = "",
value = "",
statusColor = "",
hasStatus = false,
icon = null,
}: {
label?: string;
value?: any;
hasStatus?: boolean;
statusColor: string | undefined;
icon?: any;
}) => {
return (
<Box
sx={{
alignItems: "center",
padding: "5px",
display: "flex",
gap: "10px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
flexFlow: "column",
maxWidth: "40px",
"&:first-of-type(svg)": {
fill: "#848484",
},
}}
>
{icon}
{hasStatus ? (
<Box
sx={{
marginRight: "0px",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
"& svg.min-icon": {
fill: statusColor,
width: "10px",
height: "10px",
},
}}
>
<CircleIcon />
</Box>
) : (
<Box sx={{ width: "12px", height: "12px" }} />
)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "flex-start",
flexFlow: "column",
"& .stat-text": { color: "#5E5E5E", fontSize: "14px" },
"& .stat-value": {
color: "#07193E",
display: "flex",
fontWeight: 500,
},
}}
>
<div className="stat-text">{label}</div>
<div className="stat-value">{value}</div>
</Box>
</Box>
);
};
const ServerInfoItem = ({ classes = {}, server, index }: ICardProps) => {
const networkKeys = Object.keys(get(server, "network", {}));
const networkTotal = networkKeys.length;
const totalDrives = server.drives ? server.drives.length : 0;
const activeNetwork = networkKeys.reduce((acc: number, currValue: string) => {
const item = server.network[currValue];
if (item === "online") {
return acc + 1;
}
return acc;
}, 0);
const activeDisks = server.drives
? server.drives.filter((element) => element.state === "ok").length
: 0;
return (
<Box
sx={{
display: "flex",
alignItems: "flex-start",
flexFlow: "column",
flex: 1,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: "15px",
}}
>
{server?.state && (
<Box
sx={{
marginRight: "8px",
"& .min-icon": {
fill: serverStatusColor(server.state),
height: "14px",
width: "14px",
},
}}
>
<CircleIcon />
</Box>
)}
<Box
sx={{
fontWeight: 600,
textTransform: "none",
}}
>
{server.endpoint || ""}
</Box>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
padding: "3px",
gap: "15px",
justifyContent: "space-between",
width: "100%",
paddingLeft: "20px",
flexFlow: {
sm: "row",
xs: "column",
},
}}
>
<ServerStatItem
statusColor={getDriveStatusColor(activeDisks, totalDrives)}
label={"Drives"}
icon={<DrivesIcon />}
hasStatus={true}
value={`${activeDisks}/${totalDrives}`}
/>
<ServerStatItem
statusColor={getNetworkStatusColor(activeNetwork, networkTotal)}
label={"Network"}
icon={<WarpIcon />}
hasStatus={true}
value={`${activeNetwork}/${networkTotal}`}
/>
<ServerStatItem
statusColor={"green"}
label={"Up time"}
icon={<UptimeIcon />}
value={server?.uptime ? niceDays(server.uptime) : "N/A"}
/>
<ServerStatItem
statusColor={"green"}
label={"Version"}
icon={<VersionIcon />}
value={
<Box
sx={{
background: "rgb(235, 236, 237)",
color: "#000000",
paddingLeft: "10px",
paddingRight: "10px",
borderRadius: "16px",
fontSize: "12px",
marginTop: "5px",
}}
>
{server.version ? server.version : "N/A"}
</Box>
}
/>
</Box>
</Box>
);
};
export default withStyles(styles)(ServerInfoItem);

View File

@@ -0,0 +1,146 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import ListSubheader from "@mui/material/ListSubheader";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import Collapse from "@mui/material/Collapse";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import { ServerInfo } from "../types";
import ServerInfoItem from "./ServerInfoItem";
import { Box } from "@mui/material";
import DriveInfoItem from "./DriveInfoItem";
const ServersList = ({ data }: { data: ServerInfo[] }) => {
const [expanded, setExpanded] = React.useState<string>("");
const handleClick = (key: string) => {
setExpanded(key);
};
return (
<List
sx={{ width: "100%", flex: 1 }}
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader
component="div"
sx={{
borderBottom: "1px solid #F8F8F8",
}}
>
Servers ({data.length})
</ListSubheader>
}
>
{data.map((serverInfo, index) => {
const key = `${serverInfo.endpoint}-${index}`;
const isExpanded = expanded === key;
return (
<React.Fragment key={key}>
<ListItemButton
disableRipple
onClick={() => {
if (!isExpanded) {
handleClick(key);
} else {
handleClick("");
}
}}
className={isExpanded ? "expanded" : ""}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: "1px solid #eaeaea",
"&:hover": {
background: "#F8F8F8",
},
"&.expanded": {
borderBottom: "none",
},
}}
>
<ServerInfoItem server={serverInfo} index={index} />
<Box
sx={{
height: "25px",
width: "25px",
marginLeft: "25px",
background: "#FBFAFA",
borderRadius: "2px",
"&:hover": {
background: "#fafafa",
},
display: {
md: "block",
xs: "none",
},
}}
>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</Box>
</ListItemButton>
{isExpanded ? (
<React.Fragment key={`${serverInfo.endpoint}-${index}`}>
<ListSubheader
key={`${index}-drive-details`}
component="div"
sx={{
borderBottom: "1px solid #F8F8F8",
}}
>
Drives ({serverInfo.drives.length})
</ListSubheader>
<Collapse
in={isExpanded}
timeout="auto"
unmountOnExit
sx={{
width: "100%",
flex: 1,
display: "flex",
padding: { md: "20px 50px", xs: "15px 15px" },
"& .MuiCollapse-wrapperInner": {
display: "flex",
flexFlow: "column",
gap: "15px",
},
}}
>
{serverInfo.drives.map((driveInfo, index) => {
return (
<DriveInfoItem
drive={driveInfo}
key={`${driveInfo.endpoint}-${index}`}
/>
);
})}
</Collapse>
</React.Fragment>
) : null}
</React.Fragment>
);
})}
</List>
);
};
export default ServersList;

View File

@@ -0,0 +1,147 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Box } from "@mui/material";
import { CircleIcon } from "../../../../icons";
export const StatusCountCard = ({
onlineCount = 0,
offlineCount = 0,
icon = null,
label = "",
}: {
icon: any;
onlineCount: number;
offlineCount: number;
label: string;
}) => {
return (
<Box
sx={{
fontFamily: "Lato,sans-serif",
color: "#07193E",
maxWidth: "300px",
minHeight: "200px",
display: "flex",
marginLeft: "auto",
marginRight: "auto",
cursor: "default",
}}
>
<Box
sx={{
flex: 1,
height: "200px",
display: "flex",
padding: {
sm: "0 8px 0 8px",
xs: "0 10px 0 10px",
},
}}
>
<Box
sx={{
flex: 1,
display: "flex",
flexFlow: "column",
marginTop: "32px",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
marginBottom: "24px",
}}
>
{label}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
fontSize: {
lg: "50px",
md: "45px",
xs: "35px",
},
fontWeight: 600,
"& .stat-text": { color: "#696969", fontSize: "12px" },
"& .stat-value": {
textAlign: "center",
},
"& .min-icon": {
marginRight: "8px",
height: "10px",
width: "10px",
},
}}
>
<Box>
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
fill: "#4CCB92",
},
}}
>
<CircleIcon /> <div className="stat-text">Online</div>
</Box>
<Box className="stat-value">{onlineCount}</Box>
</Box>
<Box>
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
fill: "#C83B51",
},
}}
>
<CircleIcon /> <div className="stat-text">Offline</div>
</Box>
<Box className="stat-value">{offlineCount}</Box>
</Box>
</Box>
</Box>
<Box
sx={{
width: "20px",
height: "20px",
marginTop: "26px",
maxWidth: "26px",
"& .min-icon": {
width: "16px",
height: "16px",
},
}}
>
{icon}
</Box>
</Box>
</Box>
);
};
export default StatusCountCard;

View File

@@ -0,0 +1,61 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const STATUS_COLORS = {
RED: "#C83B51",
GREEN: "#4CCB92",
YELLOW: "#E7A219",
};
export const getDriveStatusColor = (
activeDisks: number,
totalDrives: number
) => {
if (activeDisks <= totalDrives / 2) {
return STATUS_COLORS.RED;
}
if (totalDrives !== 2 && activeDisks === totalDrives / 2 + 1) {
return STATUS_COLORS.YELLOW;
}
if (activeDisks === totalDrives) {
return STATUS_COLORS.GREEN;
}
};
export const serverStatusColor = (health_status: string) => {
switch (health_status) {
case "offline":
return STATUS_COLORS.RED;
case "online":
return STATUS_COLORS.GREEN;
default:
return STATUS_COLORS.YELLOW;
}
};
export const getNetworkStatusColor = (
activeNetwork: number,
networkTotal: number
) => {
if (activeNetwork <= networkTotal / 2) {
return STATUS_COLORS.RED;
}
if (activeNetwork === networkTotal / 2 + 1) {
return STATUS_COLORS.YELLOW;
}
if (activeNetwork === networkTotal) {
return STATUS_COLORS.GREEN;
}
};

View File

@@ -24,12 +24,12 @@ import { containerForHeader } from "../Common/FormComponents/common/styleLibrary
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import BasicDashboard from "./BasicDashboard/BasicDashboard";
import { LinearProgress } from "@mui/material";
import api from "../../../common/api";
import { Usage } from "./types";
import { setErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import BasicDashboard from "./BasicDashboard/BasicDashboard";
interface IDashboardSimple {
classes: any;
@@ -82,9 +82,7 @@ const Dashboard = ({ classes, displayErrorMessage }: IDashboardSimple) => {
<PrDashboard />
</Grid>
) : (
<Grid container className={classes.container}>
<BasicDashboard usage={basicResult} />
</Grid>
<BasicDashboard usage={basicResult} />
)}
</Fragment>
)}