Added New styles to Dashboard (#1054)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net> Co-authored-by: Benjamin Perez <benjamin@bexsoft.net> Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"proxy": "http://localhost:9090/",
|
||||
"devDependencies": {
|
||||
"prettier": "2.3.1",
|
||||
"typescript": "^4.3.2"
|
||||
"prettier": "2.3.2",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ export const calculateBytes = (
|
||||
const bytes = parseInt(x, 10);
|
||||
|
||||
if (bytes === 0) {
|
||||
return { total: 0, unit: k8sCalcUnits[0] };
|
||||
return { total: 0, unit: units[0] };
|
||||
}
|
||||
|
||||
// Gi : GiB
|
||||
@@ -525,7 +525,7 @@ export const calculateBytes = (
|
||||
|
||||
// Get Unit parsed
|
||||
const unitParsed = parseFloat(roundedUnit.toFixed(fractionDigits));
|
||||
const finalUnit = k8sCalcUnits[i];
|
||||
const finalUnit = units[i];
|
||||
|
||||
return { total: unitParsed, unit: finalUnit };
|
||||
};
|
||||
|
||||
@@ -730,3 +730,107 @@ export const inlineCheckboxes = {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
};
|
||||
|
||||
const commonStateIcon = {
|
||||
marginRight: 10,
|
||||
lineHeight: 1,
|
||||
display: "inline-flex",
|
||||
marginTop: 6 ,
|
||||
};
|
||||
|
||||
export const commonDashboardInfocard = {
|
||||
cardIconContainer: {
|
||||
display: "flex" as const,
|
||||
position: "relative" as const,
|
||||
alignItems: "center" as const,
|
||||
},
|
||||
stateContainer: {
|
||||
display: "flex" as const,
|
||||
flexWrap: "wrap" as const,
|
||||
justifyContent: "flex-start" as const,
|
||||
},
|
||||
infoValue: {
|
||||
fontWeight: 500,
|
||||
color: "#07193E",
|
||||
fontSize: 16,
|
||||
margin: "8px 40px 5px 0",
|
||||
display: "inline-flex" as const,
|
||||
"& strong": {
|
||||
marginRight: 4,
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
alignItems: "center" as const,
|
||||
},
|
||||
redState: {
|
||||
color: "#F55B5B",
|
||||
...commonStateIcon,
|
||||
},
|
||||
greenState: {
|
||||
color: "#9FF281",
|
||||
...commonStateIcon,
|
||||
},
|
||||
yellowState: {
|
||||
color: "#F7A25A",
|
||||
...commonStateIcon,
|
||||
},
|
||||
greyState: {
|
||||
color: "grey",
|
||||
...commonStateIcon,
|
||||
},
|
||||
healthStatusIcon: {
|
||||
position: "absolute" as const,
|
||||
fontSize: 8,
|
||||
left: 18,
|
||||
height: 10,
|
||||
bottom: 2,
|
||||
marginRight: 10,
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 5,
|
||||
height: 5,
|
||||
},
|
||||
},
|
||||
innerState: {
|
||||
fontSize: 8,
|
||||
display: "flex" as const,
|
||||
alignItems: "center" as const,
|
||||
marginTop: -3,
|
||||
"& .MuiSvgIcon-root": {
|
||||
marginTop: 5,
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
},
|
||||
cardContainer: {
|
||||
border: "#EEF1F4 2px solid",
|
||||
borderRadius: 10,
|
||||
boxShadow: "0 0 15px #00000029",
|
||||
maxWidth: 1185,
|
||||
marginBottom: 30,
|
||||
},
|
||||
cardHeader: {
|
||||
"& .MuiCardHeader-title": {
|
||||
fontWeight: "bolder" as const,
|
||||
},
|
||||
},
|
||||
cardNumber: {
|
||||
color: "#848484",
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
referenceTitle: {
|
||||
display: "flex",
|
||||
alignItems: "center" as const,
|
||||
lineHeight: 1,
|
||||
fontWeight: "bold" as const,
|
||||
textTransform: "capitalize" as const,
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 10,
|
||||
height: 10,
|
||||
marginTop: -5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
makeStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Pagination from "@material-ui/lab/Pagination";
|
||||
|
||||
interface IGeneralUsePaginator {
|
||||
classes: any;
|
||||
page: number;
|
||||
itemsPerPage?: number;
|
||||
entity: string;
|
||||
totalItems: number;
|
||||
onChange: (newPage: number) => void;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
paginatorContainer: {
|
||||
margin: "10px 0 18px",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
paginatorInformation: {
|
||||
color: "#848484",
|
||||
fontSize: 12,
|
||||
fontStyle: "italic",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
paginatorEntity: {
|
||||
color: "#767676",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
paginationElement: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
color: "#848484",
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const paginatorStyling = makeStyles({
|
||||
ul: {
|
||||
"& .MuiPaginationItem-root": {
|
||||
color: "#07193E",
|
||||
fontSize: 14,
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "transparent",
|
||||
fontWeight: "bold",
|
||||
"&::after": {
|
||||
backgroundColor: "#07193E",
|
||||
width: "100%",
|
||||
height: 3,
|
||||
content: '" "',
|
||||
position: "absolute",
|
||||
bottom: -3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const GeneralUsePaginator = ({
|
||||
classes,
|
||||
page = 1,
|
||||
itemsPerPage = 5,
|
||||
entity,
|
||||
totalItems,
|
||||
onChange,
|
||||
}: IGeneralUsePaginator) => {
|
||||
const paginatorStyles = paginatorStyling();
|
||||
|
||||
const currentInitialItem = page * itemsPerPage - itemsPerPage + 1;
|
||||
const currentEndItem = currentInitialItem + itemsPerPage - 1;
|
||||
const displayCurrentEndItem =
|
||||
currentEndItem > totalItems ? totalItems : currentEndItem;
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Grid container className={classes.paginatorContainer}>
|
||||
<Grid item xs={6}>
|
||||
<span className={classes.paginatorEntity}>{entity}</span>
|
||||
<br />
|
||||
<span className={classes.paginatorInformation}>
|
||||
Showing{" "}
|
||||
{totalPages > 1 ? (
|
||||
<Fragment>
|
||||
{currentInitialItem} - {displayCurrentEndItem} out of{" "}
|
||||
</Fragment>
|
||||
) : null}
|
||||
{totalItems} Total {entity}
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} className={classes.paginationElement}>
|
||||
{totalPages > 1 && (
|
||||
<Fragment>
|
||||
Go to:{" "}
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
variant={"text"}
|
||||
siblingCount={3}
|
||||
page={page}
|
||||
size="small"
|
||||
hideNextButton
|
||||
hidePrevButton
|
||||
onChange={(_, newPage: number) => {
|
||||
onChange(newPage);
|
||||
}}
|
||||
classes={{ ul: paginatorStyles.ul }}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(GeneralUsePaginator);
|
||||
138
portal-ui/src/screens/Console/Common/TabSelector/TabSelector.tsx
Normal file
138
portal-ui/src/screens/Console/Common/TabSelector/TabSelector.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
// 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 {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
makeStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import Tabs from "@material-ui/core/Tabs";
|
||||
import Tab from "@material-ui/core/Tab";
|
||||
import { ITabOption } from "./types";
|
||||
|
||||
interface ITabSelector {
|
||||
selectedTab: number;
|
||||
onChange: (newValue: number) => void;
|
||||
tabOptions: ITabOption[];
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
cardsContainer: {
|
||||
maxHeight: 440,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
generalStatusCards: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
generalStatusTitle: {
|
||||
color: "#767676",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
margin: "15px 10px 0 10px",
|
||||
},
|
||||
});
|
||||
|
||||
const tabSubStyles = makeStyles({
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
wrapper: { fontSize: 22, textTransform: "uppercase", color: "#D0D0D0" },
|
||||
selected: { "& .MuiTab-wrapper": { color: "#07193E", fontWeight: "bold" } },
|
||||
indicator: {
|
||||
background:
|
||||
"transparent linear-gradient(90deg, #072B4E 0%, #081C42 100%) 0% 0% no-repeat padding-box;",
|
||||
height: 4,
|
||||
},
|
||||
scroller: {
|
||||
maxWidth: 1185,
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '" "',
|
||||
backgroundColor: "#EEF1F4",
|
||||
height: 4,
|
||||
width: "100%",
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TabSelector = ({
|
||||
selectedTab,
|
||||
onChange,
|
||||
tabOptions,
|
||||
classes,
|
||||
}: ITabSelector) => {
|
||||
const subStyles = tabSubStyles();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Tabs
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
aria-label="cluster-tabs"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
value={selectedTab}
|
||||
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
|
||||
onChange(newValue);
|
||||
}}
|
||||
classes={{
|
||||
indicator: subStyles.indicator,
|
||||
scroller: subStyles.scroller,
|
||||
}}
|
||||
>
|
||||
{tabOptions.map((option, index) => {
|
||||
let tabOptions: ITabOption = {
|
||||
label: option.label,
|
||||
};
|
||||
|
||||
if (option.value) {
|
||||
tabOptions = { ...tabOptions, value: option.value };
|
||||
}
|
||||
|
||||
if (option.disabled) {
|
||||
tabOptions = { ...tabOptions, disabled: option.disabled };
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab
|
||||
{...tabOptions}
|
||||
classes={{
|
||||
root: subStyles.root,
|
||||
wrapper: subStyles.wrapper,
|
||||
selected: subStyles.selected,
|
||||
}}
|
||||
id={`simple-tab-${index}`}
|
||||
aria-controls={`simple-tabpanel-${index}`}
|
||||
key={`tab-${index}-${option.label}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(TabSelector);
|
||||
21
portal-ui/src/screens/Console/Common/TabSelector/types.ts
Normal file
21
portal-ui/src/screens/Console/Common/TabSelector/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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/>.
|
||||
|
||||
export interface ITabOption {
|
||||
label: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -14,44 +14,42 @@
|
||||
// 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 React, { Fragment, useState } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Usage } from "../types";
|
||||
import { niceBytes } from "../../../../common/utils";
|
||||
import { IDriveInfo, Usage } from "../types";
|
||||
import { calculateBytes } from "../../../../common/utils";
|
||||
import { TabPanel } from "../../../shared/tabs";
|
||||
import ReportedUsageIcon from "../../../../icons/ReportedUsageIcon";
|
||||
import ServerInfoCard from "./ServerInfoCard";
|
||||
import DriveInfoCard from "./DriveInfoCard";
|
||||
import {
|
||||
BucketsIcon,
|
||||
ServersIcon,
|
||||
StorageIcon,
|
||||
TotalObjectsIcon,
|
||||
VersionIcon,
|
||||
} from "../../../../icons";
|
||||
import { Card, CardHeader } from "@material-ui/core";
|
||||
import { BucketsIcon, TotalObjectsIcon } from "../../../../icons";
|
||||
import CommonCard from "../CommonCard";
|
||||
import TabSelector from "../../Common/TabSelector/TabSelector";
|
||||
import GeneralUsePaginator from "../../Common/GeneralUsePaginator/GeneralUsePaginator";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
dashboardBG: {
|
||||
width: 390,
|
||||
height: 255,
|
||||
zIndex: -1,
|
||||
position: "fixed",
|
||||
backgroundSize: "fill",
|
||||
backgroundImage: "url(/images/BG_IllustrationDarker.svg)",
|
||||
backgroundPosition: "right bottom",
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundRepeat: "no-repeat",
|
||||
},
|
||||
|
||||
cardsContainer: {
|
||||
maxHeight: 440,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
generalStatusCards: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
generalStatusTitle: {
|
||||
color: "#767676",
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
margin: "15px 10px 0 10px",
|
||||
},
|
||||
paginatorContainer: {
|
||||
maxWidth: 1185,
|
||||
width: "100%",
|
||||
},
|
||||
});
|
||||
|
||||
interface IDashboardProps {
|
||||
@@ -59,24 +57,21 @@ interface IDashboardProps {
|
||||
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 "0";
|
||||
return { total: "0", unit: "Mi" };
|
||||
}
|
||||
|
||||
const niceBytesUsage = niceBytes(usage).split(" ");
|
||||
const calculatedBytes = calculateBytes(usage);
|
||||
|
||||
if (niceBytesUsage.length !== 2) {
|
||||
return niceBytesUsage.join(" ");
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{niceBytesUsage[0]}
|
||||
<span className={classes.smallUnit}>{niceBytesUsage[1]}</span>
|
||||
</Fragment>
|
||||
);
|
||||
return calculatedBytes;
|
||||
};
|
||||
|
||||
const prettyNumber = (usage: number | undefined) => {
|
||||
@@ -105,95 +100,101 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
|
||||
|
||||
const serverArray = makeServerArray(usage);
|
||||
|
||||
const usageToRepresent = prettyUsage(usage ? usage.usage.toString() : "0");
|
||||
|
||||
let allDrivesArray: IDriveInfo[] = [];
|
||||
|
||||
serverArray.forEach((server) => {
|
||||
const drivesInput = server.drives.map((drive) => {
|
||||
return drive;
|
||||
});
|
||||
|
||||
allDrivesArray = [...allDrivesArray, ...drivesInput];
|
||||
});
|
||||
|
||||
const splitedServers = serverArray.slice(
|
||||
serversPageNumber * itemsPerPage - itemsPerPage,
|
||||
serversPageNumber * itemsPerPage
|
||||
);
|
||||
|
||||
const splitedDrives = allDrivesArray.slice(
|
||||
drivesPageNumber * itemsPerPage - itemsPerPage,
|
||||
drivesPageNumber * itemsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.dashboardBG} />
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardHeader
|
||||
avatar={<BucketsIcon />}
|
||||
title="Number of Buckets"
|
||||
subheader={usage ? prettyNumber(usage.buckets) : 0}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardHeader
|
||||
avatar={<ReportedUsageIcon />}
|
||||
title="Usage"
|
||||
subheader={usage ? prettyUsage(usage.usage + "") : 0}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardHeader
|
||||
avatar={<TotalObjectsIcon />}
|
||||
title="Total Objects"
|
||||
subheader={usage ? prettyNumber(usage.objects) : 0}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Card className={classes.cardRoot}>
|
||||
{usage
|
||||
? usage.servers.length !== 0 && (
|
||||
<CardHeader
|
||||
avatar={<VersionIcon />}
|
||||
title="MinIO Version"
|
||||
subheader={usage ? usage.servers[0].version : 0}
|
||||
/>
|
||||
)
|
||||
: 0}
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={6} />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Grid container alignItems="center" spacing={2}>
|
||||
<Grid item>
|
||||
<StorageIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h5">Drives Status</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} className={classes.cardsContainer}>
|
||||
{serverArray.map((server, index) =>
|
||||
server.drives.map((drive) => (
|
||||
<Grid item xs={12} key={drive.uuid}>
|
||||
<DriveInfoCard drive={drive} />
|
||||
</Grid>
|
||||
))
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.generalStatusTitle}>
|
||||
General Status
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Grid container alignItems="center" spacing={2}>
|
||||
<Grid item>
|
||||
<ServersIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h5">Servers Status</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1}>
|
||||
{serverArray.map((server, index) => (
|
||||
<Grid item xs={12}>
|
||||
<ServerInfoCard
|
||||
server={server}
|
||||
key={`serverDS-${index.toString()}`}
|
||||
/>
|
||||
<Grid item xs={12} className={classes.generalStatusCards}>
|
||||
<CommonCard
|
||||
avatar={<BucketsIcon />}
|
||||
title={"All Buckets"}
|
||||
metricValue={usage ? prettyNumber(usage.buckets) : 0}
|
||||
/>
|
||||
<CommonCard
|
||||
avatar={<ReportedUsageIcon />}
|
||||
title={"Usage"}
|
||||
metricValue={usageToRepresent.total}
|
||||
metricUnit={usageToRepresent.unit}
|
||||
/>
|
||||
<CommonCard
|
||||
avatar={<TotalObjectsIcon />}
|
||||
title={"Total Objects"}
|
||||
metricValue={usage ? prettyNumber(usage.objects) : 0}
|
||||
/>
|
||||
<CommonCard
|
||||
avatar={<TotalObjectsIcon />}
|
||||
title={"Servers"}
|
||||
metricValue={usage ? prettyNumber(serverArray.length) : 0}
|
||||
subMessage={{ message: "Total" }}
|
||||
/>
|
||||
</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>
|
||||
))}
|
||||
</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>
|
||||
</Fragment>
|
||||
|
||||
@@ -20,65 +20,12 @@ import Grid from "@material-ui/core/Grid";
|
||||
import { IDriveInfo } from "../types";
|
||||
import { niceBytes } from "../../../../common/utils";
|
||||
import { Card, CardHeader } from "@material-ui/core";
|
||||
import { CircleIcon, StorageIcon } from "../../../../icons";
|
||||
import { CircleIcon } from "../../../../icons";
|
||||
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
cardIconContainer: {
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
alignItems: "center",
|
||||
},
|
||||
stateContainer: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
infoValue: {
|
||||
fontWeight: 500,
|
||||
color: "#777777",
|
||||
fontSize: 14,
|
||||
margin: "5px 4px",
|
||||
display: "inline-flex",
|
||||
"& strong": {
|
||||
marginRight: 4,
|
||||
},
|
||||
},
|
||||
redState: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
greenState: {
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
yellowState: {
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
greyState: {
|
||||
color: "grey",
|
||||
},
|
||||
healthStatusIcon: {
|
||||
position: "absolute",
|
||||
fontSize: 10,
|
||||
left: 18,
|
||||
height: 10,
|
||||
bottom: 2,
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
},
|
||||
innerState: {
|
||||
fontSize: 10,
|
||||
marginLeft: 5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginTop: -3,
|
||||
},
|
||||
cardHeader: {
|
||||
"& .MuiCardHeader-title": {
|
||||
fontWeight: "bolder",
|
||||
},
|
||||
},
|
||||
...commonDashboardInfocard,
|
||||
});
|
||||
|
||||
interface ICardProps {
|
||||
@@ -100,22 +47,19 @@ const DriveInfoCard = ({ classes, drive }: ICardProps) => {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card>
|
||||
<Card className={classes.cardContainer}>
|
||||
<CardHeader
|
||||
className={classes.cardHeader}
|
||||
avatar={
|
||||
<div className={classes.cardIconContainer}>
|
||||
<StorageIcon className="computerIcon" />
|
||||
<div className={classes.healthStatusIcon}>
|
||||
{drive.state && (
|
||||
<span className={driveStatusToClass(drive.state)}>
|
||||
<CircleIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
title={
|
||||
<div className={classes.referenceTitle}>
|
||||
{drive.state && (
|
||||
<span className={driveStatusToClass(drive.state)}>
|
||||
<CircleIcon />
|
||||
</span>
|
||||
)}
|
||||
{drive.endpoint || ""}
|
||||
</div>
|
||||
}
|
||||
title={drive.endpoint || ""}
|
||||
subheader={
|
||||
<Grid item xs={12} className={classes.stateContainer}>
|
||||
<span className={classes.infoValue}>
|
||||
|
||||
@@ -16,83 +16,28 @@
|
||||
|
||||
import React from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import ComputerIcon from "@material-ui/icons/Computer";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { ServerInfo } from "../types";
|
||||
import { niceDays } from "../../../../common/utils";
|
||||
import { Card, CardHeader } from "@material-ui/core";
|
||||
import { CircleIcon } from "../../../../icons";
|
||||
import { CircleIcon, VersionIcon } from "../../../../icons";
|
||||
import get from "lodash/get";
|
||||
import { commonDashboardInfocard } from "../../Common/FormComponents/common/styleLibrary";
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
cardIconContainer: {
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
alignItems: "center",
|
||||
},
|
||||
stateContainer: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
infoValue: {
|
||||
fontWeight: 500,
|
||||
color: "#777777",
|
||||
fontSize: 14,
|
||||
margin: "5px 4px",
|
||||
display: "inline-flex",
|
||||
"& strong": {
|
||||
marginRight: 4,
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
},
|
||||
redState: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
greenState: {
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
yellowState: {
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
greyState: {
|
||||
color: "grey",
|
||||
},
|
||||
healthStatusIcon: {
|
||||
position: "absolute",
|
||||
fontSize: 10,
|
||||
left: 18,
|
||||
height: 10,
|
||||
bottom: 2,
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
},
|
||||
innerState: {
|
||||
fontSize: 10,
|
||||
marginLeft: 5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginTop: -3,
|
||||
},
|
||||
cardHeader: {
|
||||
"& .MuiCardHeader-title": {
|
||||
fontWeight: "bolder",
|
||||
},
|
||||
},
|
||||
...commonDashboardInfocard,
|
||||
});
|
||||
|
||||
interface ICardProps {
|
||||
classes: any;
|
||||
server: ServerInfo;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const ServerInfoCard = ({ classes, server }: ICardProps) => {
|
||||
const ServerInfoCard = ({ classes, server, index }: ICardProps) => {
|
||||
const serverStatusToClass = (health_status: string) => {
|
||||
switch (health_status) {
|
||||
case "offline":
|
||||
@@ -122,26 +67,25 @@ const ServerInfoCard = ({ classes, server }: ICardProps) => {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className={classes.cardContainer}>
|
||||
<CardHeader
|
||||
className={classes.cardHeader}
|
||||
avatar={
|
||||
<div className={classes.cardIconContainer}>
|
||||
<ComputerIcon className="computerIcon" />
|
||||
<div className={classes.healthStatusIcon}>
|
||||
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>
|
||||
}
|
||||
title={server.endpoint || ""}
|
||||
subheader={
|
||||
<Grid item xs={12} className={classes.stateContainer}>
|
||||
<span className={classes.infoValue}>
|
||||
<strong>Drives:</strong> {activeDisks}/{totalDrives}{" "}
|
||||
<span
|
||||
className={`${classes.innerState} ${
|
||||
activeDisks <= totalDrives / 2 && classes.redState
|
||||
@@ -151,9 +95,9 @@ const ServerInfoCard = ({ classes, server }: ICardProps) => {
|
||||
>
|
||||
<CircleIcon />
|
||||
</span>
|
||||
Drives: {activeDisks}/{totalDrives}{" "}
|
||||
</span>
|
||||
<span className={classes.infoValue}>
|
||||
<strong>Network:</strong> {activeNetwork}/{networkTotal}{" "}
|
||||
<span
|
||||
className={`${classes.innerState} ${
|
||||
activeNetwork <= networkTotal / 2 && classes.redState
|
||||
@@ -163,10 +107,14 @@ const ServerInfoCard = ({ classes, server }: ICardProps) => {
|
||||
>
|
||||
<CircleIcon />
|
||||
</span>
|
||||
Network: {activeNetwork}/{networkTotal}{" "}
|
||||
</span>
|
||||
<span className={classes.infoValue}>
|
||||
<strong>Uptime:</strong>{" "}
|
||||
{server.uptime ? niceDays(server.uptime) : "N/A"}
|
||||
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>
|
||||
}
|
||||
|
||||
190
portal-ui/src/screens/Console/Dashboard/CommonCard.tsx
Normal file
190
portal-ui/src/screens/Console/Dashboard/CommonCard.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
// 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 { Card, CardHeader } from "@material-ui/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
makeStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
export interface ISubInterface {
|
||||
message: string;
|
||||
fontWeight?: "normal" | "bold";
|
||||
}
|
||||
|
||||
interface ICommonCard {
|
||||
avatar: any;
|
||||
title: string;
|
||||
metricValue: any;
|
||||
metricUnit?: string;
|
||||
subMessage?: ISubInterface;
|
||||
moreLink?: string;
|
||||
rightComponent?: any;
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
cardRoot: {
|
||||
border: "#eef1f4 2px solid",
|
||||
borderRadius: 10,
|
||||
maxWidth: 280,
|
||||
width: "100%",
|
||||
margin: 10,
|
||||
},
|
||||
cardsContainer: {
|
||||
maxHeight: 440,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
metricText: {
|
||||
fontSize: 70,
|
||||
lineHeight: 1.1,
|
||||
color: "#07193E",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 10,
|
||||
color: "#767676",
|
||||
fontWeight: "normal",
|
||||
},
|
||||
subHearderContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
subMessage: {
|
||||
fontSize: 10,
|
||||
color: "#767676",
|
||||
"&.bold": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
headerContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
viewAll: {
|
||||
fontSize: 10,
|
||||
color: "#C83B51",
|
||||
textTransform: "capitalize",
|
||||
|
||||
"& a, & a:hover, & a:visited, & a:active": {
|
||||
color: "#C83B51",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cardSubStyles = makeStyles({
|
||||
root: { backgroundColor: "#fff" },
|
||||
title: {
|
||||
color: "#404144",
|
||||
fontSize: 14,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: "bold",
|
||||
borderBottom: "#eef1f4 1px solid",
|
||||
paddingBottom: 14,
|
||||
marginBottom: 5,
|
||||
},
|
||||
content: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
});
|
||||
|
||||
const CommonCard = ({
|
||||
avatar,
|
||||
title,
|
||||
metricValue,
|
||||
metricUnit,
|
||||
subMessage,
|
||||
moreLink,
|
||||
rightComponent,
|
||||
classes,
|
||||
}: ICommonCard) => {
|
||||
const subStyles = cardSubStyles();
|
||||
const SubHeader = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.subHearderContainer}>
|
||||
<div className={classes.leftSide}>
|
||||
<div>
|
||||
<span className={classes.metricText}>
|
||||
{metricValue}
|
||||
<span className={classes.unitText}>{metricUnit}</span>
|
||||
</span>
|
||||
</div>
|
||||
{subMessage && (
|
||||
<div
|
||||
className={`${classes.subMessage} ${
|
||||
subMessage.fontWeight ? subMessage.fontWeight : ""
|
||||
}`}
|
||||
>
|
||||
{subMessage.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.rightSide}>{rightComponent}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.headerContainer}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
{moreLink && (
|
||||
<Fragment>
|
||||
<span className={classes.viewAll}>
|
||||
<Link to={moreLink}>View All</Link>
|
||||
</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card className={classes.cardRoot}>
|
||||
{metricValue !== "" && (
|
||||
<CardHeader
|
||||
title={<Header />}
|
||||
subheader={
|
||||
<Fragment>
|
||||
<SubHeader />
|
||||
</Fragment>
|
||||
}
|
||||
classes={{
|
||||
root: subStyles.root,
|
||||
title: subStyles.title,
|
||||
content: subStyles.content,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(CommonCard);
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
|
||||
interface ISimpleWidget {
|
||||
classes: any;
|
||||
iconWidget: any;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
mainWidgetContainer: {
|
||||
display: "inline-flex",
|
||||
color: "#072A4D",
|
||||
alignItems: "center",
|
||||
},
|
||||
icon: {
|
||||
color: "#072A4D",
|
||||
fill: "#072A4D",
|
||||
marginRight: 5,
|
||||
marginLeft: 12,
|
||||
},
|
||||
widgetLabel: {
|
||||
fontWeight: "bold",
|
||||
textTransform: "uppercase",
|
||||
marginRight: 10,
|
||||
},
|
||||
widgetValue: {
|
||||
marginRight: 25,
|
||||
}
|
||||
});
|
||||
|
||||
const SimpleWidget = ({ classes, iconWidget, label, value }: ISimpleWidget) => {
|
||||
return (
|
||||
<span className={classes.mainWidgetContainer}>
|
||||
<span className={classes.icon}>{iconWidget ? iconWidget : null}</span>
|
||||
<span className={classes.widgetLabel}>{label}: </span>
|
||||
<span className={classes.widgetValue}>{value}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(SimpleWidget);
|
||||
@@ -106,7 +106,7 @@ const Hop = ({ classes, match }: IHopSimple) => {
|
||||
const next = `${loc}${add}cp=y`;
|
||||
consoleFrame.current.contentDocument.location.replace(next);
|
||||
} else {
|
||||
consoleFrame.current.contentDocument.location.reload(true);
|
||||
consoleFrame.current.contentDocument.location.reload();
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -9306,10 +9306,10 @@ prepend-http@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
|
||||
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
|
||||
prettier@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
|
||||
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
|
||||
|
||||
pretty-bytes@^5.3.0:
|
||||
version "5.6.0"
|
||||
@@ -11499,10 +11499,10 @@ typeface-roboto@^0.0.75:
|
||||
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
|
||||
integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
|
||||
|
||||
typescript@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
|
||||
integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
|
||||
typescript@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
|
||||
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
|
||||
|
||||
unbox-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
||||
Reference in New Issue
Block a user