Operator improvements (#1798)

Added new design to Tenants page list
Added Pool details initial page

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-04-05 10:27:54 -06:00
committed by GitHub
parent 822724a4f1
commit 64ffa039b4
21 changed files with 2002 additions and 252 deletions

2
go.mod
View File

@@ -22,7 +22,7 @@ require (
github.com/minio/madmin-go v1.3.5
github.com/minio/mc v0.0.0-20220302011226-f13defa54577
github.com/minio/minio-go/v7 v7.0.23
github.com/minio/operator v0.0.0-20220322220228-ae7a32a7c19e
github.com/minio/operator v0.0.0-20220401213108-1e35dbf22c40
github.com/minio/pkg v1.1.20
github.com/minio/selfupdate v0.4.0
github.com/mitchellh/go-homedir v1.1.0

967
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,9 @@ package models
import (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@@ -70,6 +72,9 @@ type TenantList struct {
// pool count
PoolCount int64 `json:"pool_count,omitempty"`
// tiers
Tiers []*TenantTierElement `json:"tiers"`
// total size
TotalSize int64 `json:"total_size,omitempty"`
@@ -79,11 +84,75 @@ type TenantList struct {
// Validate validates this tenant list
func (m *TenantList) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateTiers(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContextValidate validates this tenant list based on context it is used
func (m *TenantList) validateTiers(formats strfmt.Registry) error {
if swag.IsZero(m.Tiers) { // not required
return nil
}
for i := 0; i < len(m.Tiers); i++ {
if swag.IsZero(m.Tiers[i]) { // not required
continue
}
if m.Tiers[i] != nil {
if err := m.Tiers[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tiers" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tiers" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this tenant list based on the context it is used
func (m *TenantList) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateTiers(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *TenantList) contextValidateTiers(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Tiers); i++ {
if m.Tiers[i] != nil {
if err := m.Tiers[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tiers" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tiers" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,73 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// TenantTierElement tenant tier element
//
// swagger:model tenantTierElement
type TenantTierElement struct {
// name
Name string `json:"name,omitempty"`
// size
Size int64 `json:"size,omitempty"`
// type
Type string `json:"type,omitempty"`
}
// Validate validates this tenant tier element
func (m *TenantTierElement) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this tenant tier element based on context it is used
func (m *TenantTierElement) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *TenantTierElement) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *TenantTierElement) UnmarshalBinary(b []byte) error {
var res TenantTierElement
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -3454,6 +3454,12 @@ func init() {
"pool_count": {
"type": "integer"
},
"tiers": {
"type": "array",
"items": {
"$ref": "#/definitions/tenantTierElement"
}
},
"total_size": {
"type": "integer"
},
@@ -3696,6 +3702,21 @@ func init() {
}
}
},
"tenantTierElement": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"type": {
"type": "string"
}
}
},
"tenantUsage": {
"type": "object",
"properties": {
@@ -8044,6 +8065,12 @@ func init() {
"pool_count": {
"type": "integer"
},
"tiers": {
"type": "array",
"items": {
"$ref": "#/definitions/tenantTierElement"
}
},
"total_size": {
"type": "integer"
},
@@ -8286,6 +8313,21 @@ func init() {
}
}
},
"tenantTierElement": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"type": {
"type": "string"
}
}
},
"tenantUsage": {
"type": "object",
"properties": {

View File

@@ -808,6 +808,18 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
deletion = tenant.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)
}
var tiers []*models.TenantTierElement
for _, tier := range tenant.Status.Usage.Tiers {
tierItem := &models.TenantTierElement{
Name: tier.Name,
Type: tier.Type,
Size: tier.TotalSize,
}
tiers = append(tiers, tierItem)
}
tenants = append(tenants, &models.TenantList{
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
@@ -823,6 +835,7 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
CapacityRawUsage: tenant.Status.Usage.RawUsage,
Capacity: tenant.Status.Usage.Capacity,
CapacityUsage: tenant.Status.Usage.Usage,
Tiers: tiers,
})
}

View File

@@ -0,0 +1,35 @@
// 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 EditTenantIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<g transform="translate(0 -0.853)">
<path d="M89.25,173.48c-2.67-.25-5.25-1.12-7.54-2.52-2.52-2.16-3.51-5.62-2.52-8.78l7.55-35.2L204.84,8.87C210.17,4.17,216.73,1.09,223.76,0c7.06-.19,13.88,2.53,18.86,7.54,10.33,11.14,9.77,28.52-1.26,38.97l-116.9,118.1-33.94,7.55-1.26,1.25v.07Zm12.58-37.71l-5.04,20.12,20.13-5.03L231.28,36.46c4.78-4.21,5.34-11.46,1.26-16.35-2.52-2.52-5.03-3.77-7.54-2.52-3.34-.09-6.56,1.3-8.8,3.78l-114.39,114.39h.01Z" />
<path d="M179.76,227.54H23.88C10.69,227.54,0,216.84,0,203.65V47.78c0-13.19,10.69-23.88,23.88-23.88H108.1v15.07H23.88c-4.46,.46-7.77,4.34-7.54,8.81V203.65c-.24,4.47,3.08,8.34,7.54,8.8H179.76c4.75,.12,8.69-3.63,8.81-8.38,0-.14,0-.28,0-.42v-49.03h16.33v49.03c-1.03,13.25-11.92,23.57-25.21,23.88h.07Z" />
</g>
</svg>
);
export default EditTenantIcon;

View File

@@ -182,3 +182,4 @@ export { default as SelectAllIcon } from "./SelectAllIcon";
export { default as BackIcon } from "./BackIcon";
export { default as DeleteNonCurrentIcon } from "./DeleteNonCurrentIcon";
export { default as FilterIcon } from "./FilterIcon";
export { default as EditTenantIcon } from "./EditTenantIcon";

View File

@@ -403,6 +403,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
EditIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.EditTenantIcon />
<br />
EditTenantIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.EditYamlIcon />
<br />
@@ -1075,7 +1081,8 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
WatchIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.AlertCloseIcon /> <br />
<cicons.AlertCloseIcon />
<br />
AlertCloseIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>

View File

@@ -0,0 +1,57 @@
// 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, { Fragment } from "react";
interface IInformationItemProps {
label: string;
value: string;
unit?: string;
}
const InformationItem = ({ label, value, unit }: IInformationItemProps) => {
return (
<div style={{ margin: "0px 20px" }}>
<div style={{ textAlign: "center" }}>
<span style={{ fontSize: 18, color: "#000", fontWeight: 400 }}>
{value}
</span>
{unit && (
<Fragment>
{" "}
<span
style={{ fontSize: 12, color: "#8F9090", fontWeight: "bold" }}
>
{unit}
</span>
</Fragment>
)}
</div>
<div
style={{
textAlign: "center",
color: "#767676",
fontSize: 12,
whiteSpace: "nowrap",
}}
>
{label}
</div>
</div>
);
};
export default InformationItem;

View File

@@ -98,11 +98,6 @@ const styles = (theme: Theme) =>
marginTop: 25,
height: "calc(100vh - 195px)",
},
searchField: {
...searchField.searchField,
marginRight: "auto",
maxWidth: 380,
},
});
const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
@@ -191,22 +186,17 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
)}
<PageHeader
label="Tenants"
middleComponent={
<SearchBox
placeholder={"Filter Tenants"}
onChange={(val) => {
setFilterTenants(val);
}}
value={filterTenants}
/>
}
actions={
<Grid
item
xs={12}
className={classes.actionsTray}
marginRight={"30px"}
>
<SearchBox
placeholder={"Filter Tenants"}
onChange={(val) => {
setFilterTenants(val);
}}
overrideClass={classes.searchField}
value={filterTenants}
/>
<Grid item xs={12} marginRight={"30px"}>
<RBIconButton
id={"refresh-tenant-list"}
tooltip={"Refresh Tenant List"}

View File

@@ -0,0 +1,148 @@
// 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 { Cell, Pie, PieChart } from "recharts";
import { CapacityValue, CapacityValues } from "./types";
import { niceBytesInt } from "../../../../common/utils";
import { CircleIcon } from "../../../../icons";
interface ITenantCapacity {
totalCapacity: number;
usedSpaceVariants: CapacityValues[];
statusClass: string;
}
const TenantCapacity = ({
totalCapacity,
usedSpaceVariants,
statusClass,
}: ITenantCapacity) => {
const colors = [
"#C4D4E9",
"#DCD1EE",
"#D1EEE7",
"#EEDED1",
"#AAF38F",
"#F9E6C5",
"#71ACCB",
"#F4CECE",
"#D6D6D6",
"#2781B0",
];
const totalUsedSpace = usedSpaceVariants.reduce((acc, currValue) => {
return acc + currValue.value;
}, 0);
const emptySpace = totalCapacity - totalUsedSpace;
let tiersList: CapacityValue[] = [];
const standardTier = usedSpaceVariants.find(
(tier) => tier.variant === "STANDARD"
) || {
value: 0,
variant: "empty",
};
if (usedSpaceVariants.length > 10) {
const totalUsedByTiers = totalUsedSpace - standardTier.value;
tiersList = [
{ value: totalUsedByTiers, color: "#2781B0", label: "Total Tiers Space" },
];
} else {
tiersList = usedSpaceVariants
.filter((variant) => variant.variant !== "STANDARD")
.map((variant, index) => {
return {
value: variant.value,
color: colors[index],
label: `Tier - ${variant.variant}`,
};
});
}
let standardTierColor = "#07193E";
const usedPercentage = (standardTier.value * 100) / totalCapacity;
if (usedPercentage >= 90) {
standardTierColor = "#C83B51";
} else if (usedPercentage >= 75) {
standardTierColor = "#FFAB0F";
}
const plotValues: CapacityValue[] = [
{ value: emptySpace, color: "#D6D6D6", label: "Empty Space" },
{
value: standardTier.value,
color: standardTierColor,
label: "Used Space by Tenant",
},
...tiersList,
];
return (
<div style={{ position: "relative", width: 110, height: 110 }}>
<div
style={{ position: "absolute", right: -5, top: 15, zIndex: 400 }}
className={statusClass}
>
<CircleIcon
style={{
border: "#fff 2px solid",
borderRadius: "100%",
width: 20,
height: 20,
}}
/>
</div>
<span
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
fontWeight: "bold",
color: "#000",
fontSize: 12,
}}
>
{niceBytesInt(totalUsedSpace)}
</span>
<div>
<PieChart width={110} height={110}>
<Pie
data={plotValues}
cx={"50%"}
cy={"50%"}
dataKey="value"
outerRadius={50}
innerRadius={40}
>
{plotValues.map((entry, index) => (
<Cell key={`cellCapacity-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</div>
</div>
);
};
export default TenantCapacity;

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
@@ -15,20 +15,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { ITenant, ValueUnit } from "./types";
import { CapacityValues, ITenant, ValueUnit } from "./types";
import { connect } from "react-redux";
import { setErrorSnackMessage } from "../../../../actions";
import Grid from "@mui/material/Grid";
import { ArrowRightIcon, CircleIcon, SettingsIcon } from "../../../../icons";
import history from "../../../../history";
import TenantsIcon from "../../../../icons/TenantsIcon";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { niceBytes } from "../../../../common/utils";
import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper";
import {niceBytes, niceBytesInt} from "../../../../common/utils";
import { tenantIsOnline } from "./utils";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { Button } from "@mui/material";
import InformationItem from "./InformationItem";
import TenantCapacity from "./TenantCapacity";
const styles = (theme: Theme) =>
createStyles({
@@ -77,15 +76,22 @@ const styles = (theme: Theme) =>
height: 10,
},
tenantItem: {
border: "1px solid #dedede",
border: "1px solid #EAEDEE",
borderRadius: 3,
marginBottom: 20,
paddingLeft: 40,
paddingRight: 40,
paddingTop: 30,
paddingBottom: 30,
padding: "15px 30px",
"&:hover": {
backgroundColor: "#FAFAFA",
cursor: "pointer",
},
},
titleContainer: {
display: "flex",
justifyContent: "space-between",
width: "100%",
},
title: {
fontSize: 22,
fontSize: 18,
fontWeight: "bold",
},
titleSubKey: {
@@ -112,6 +118,18 @@ const styles = (theme: Theme) =>
marginRight: 8,
textTransform: "initial",
},
namespaceLabel: {
display: "inline-flex",
backgroundColor: "#EAEDEF",
borderRadius: 2,
padding: "4px 8px",
fontSize: 10,
marginRight: 20,
},
status: {
fontSize: 12,
color: "#8F9090",
},
});
interface ITenantListItem {
@@ -133,9 +151,9 @@ const TenantListItem = ({ tenant, classes }: ITenantListItem) => {
}
};
var raw: ValueUnit = { value: "n/a", unit: "" };
var capacity: ValueUnit = { value: "n/a", unit: "" };
var used: ValueUnit = { value: "n/a", unit: "" };
let raw: ValueUnit = { value: "n/a", unit: "" };
let capacity: ValueUnit = { value: "n/a", unit: "" };
let used: ValueUnit = { value: "n/a", unit: "" };
if (tenant.capacity_raw) {
const b = niceBytes(`${tenant.capacity_raw}`, true);
@@ -150,126 +168,128 @@ const TenantListItem = ({ tenant, classes }: ITenantListItem) => {
capacity.unit = parts[1];
}
if (tenant.capacity_usage) {
const usageProportion =
(tenant.capacity! * tenant.capacity_raw_usage!) / tenant.capacity_raw!;
const b = niceBytes(`${usageProportion}`, true);
const b = niceBytesInt(tenant.capacity_usage, true);
const parts = b.split(" ");
used.value = parts[0];
used.unit = parts[1];
}
let spaceVariants: CapacityValues[] = [];
if (!tenant.tiers || tenant.tiers.length === 0) {
spaceVariants = [{ value: tenant.capacity_usage || 0, variant: "STANDARD" }];
} else {
spaceVariants = tenant.tiers.map((itemTenant) => {
return { value: itemTenant.size, variant: itemTenant.name };
});
}
const openTenantDetails = () => {
history.push(`/namespaces/${tenant.namespace}/tenants/${tenant.name}`);
};
return (
<Fragment>
<div className={classes.tenantItem} id={`list-tenant-${tenant.name}`}>
<div
className={classes.tenantItem}
id={`list-tenant-${tenant.name}`}
onClick={openTenantDetails}
>
<Grid container>
<Grid item xs={8}>
<div className={classes.title}>{tenant.name}</div>
<Grid item xs={12} className={classes.titleContainer}>
<div className={classes.title}>
<span>{tenant.name}</span>
</div>
<div>
<span className={classes.titleSubKey}>Namespace:</span>
<span className={classes.titleSubValue}>{tenant.namespace}</span>
<span className={classes.titleSubKey}>Pools:</span>
<span className={classes.titleSubValue}>{tenant.pool_count}</span>
<span className={classes.titleSubKey}>State:</span>
<span className={classes.titleSubValue}>
{tenant.currentState}
<span className={classes.namespaceLabel}>
Namespace:&nbsp;{tenant.namespace}
</span>
</div>
</Grid>
<Grid item xs={4} textAlign={"end"}>
<RBIconButton
id={"manage-tenant-" + tenant.name}
tooltip={"Manage Tenant"}
text={"Manage"}
disabled={!tenantIsOnline(tenant)}
onClick={() => {
history.push(
`/namespaces/${tenant.namespace}/tenants/${tenant.name}/hop`
);
}}
icon={<SettingsIcon />}
color="primary"
variant={"outlined"}
/>
<RBIconButton
id={"view-tenant-" + tenant.name}
tooltip={"View Tenant"}
text={"View"}
onClick={() => {
history.push(
`/namespaces/${tenant.namespace}/tenants/${tenant.name}`
);
}}
icon={<ArrowRightIcon />}
color="primary"
variant={"contained"}
/>
</Grid>
<Grid item xs={12}>
<hr />
</Grid>
<Grid item xs={12}>
<Grid container alignItems={"center"}>
<Grid item xs={7}>
<Grid container>
<Grid item xs={3} style={{ textAlign: "center" }}>
<div className={classes.tenantIcon}>
<TenantsIcon style={{ height: 40, width: 40 }} />
<div className={classes.healthStatusIcon}>
<span
className={healthStatusToClass(tenant.health_status)}
>
<CircleIcon />
</span>
</div>
</div>
</Grid>
<Grid item xs={3}>
<Grid container>
<Grid item xs={12} className={classes.boxyTitle}>
Raw Capacity
</Grid>
<Grid item className={classes.boxyValue}>
{raw.value}
<span className={classes.boxyUnit}>{raw.unit}</span>
</Grid>
</Grid>
</Grid>
<Grid item xs={3}>
<Grid container>
<Grid item xs={12} className={classes.boxyTitle}>
Capacity
</Grid>
<Grid item className={classes.boxyValue}>
{capacity.value}
<span className={classes.boxyUnit}>
{capacity.unit}
</span>
</Grid>
</Grid>
</Grid>
<Grid item xs={3}>
<Grid container>
<Grid item xs={12} className={classes.boxyTitle}>
Usage
</Grid>
<Grid item className={classes.boxyValue}>
{used.value}
<span className={classes.boxyUnit}>{used.unit}</span>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sx={{ marginTop: 2 }}>
<Grid container>
<Grid item xs={2}>
<TenantCapacity
totalCapacity={tenant.capacity_raw || 0}
usedSpaceVariants={spaceVariants}
statusClass={healthStatusToClass(tenant.health_status)}
/>
</Grid>
<Grid item xs>
<Grid
item
xs
sx={{
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
marginTop: "10px",
}}
>
<InformationItem
label={"Raw Capacity"}
value={raw.value}
unit={raw.unit}
/>
<InformationItem
label={"Usable Capacity"}
value={capacity.value}
unit={capacity.unit}
/>
<InformationItem
label={"Usage"}
value={used.value}
unit={used.unit}
/>
<InformationItem
label={"Pools"}
value={tenant.pool_count.toString()}
/>
</Grid>
<Grid
item
xs={12}
sx={{ paddingLeft: "20px", marginTop: "15px" }}
>
<span className={classes.status}>
<strong>State:</strong> {tenant.currentState}
</span>
</Grid>
</Grid>
<Grid item xs={5}>
<UsageBarWrapper
currValue={tenant.capacity_raw_usage ?? 0}
maxValue={tenant.capacity_raw ?? 1}
label={""}
renderFunction={niceBytes}
error={""}
loading={false}
labels={false}
/>
<Grid
item
xs={2}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<Button
id={"manage-tenant-" + tenant.name}
disabled={!tenantIsOnline(tenant)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
history.push(
`/namespaces/${tenant.namespace}/tenants/${tenant.name}/hop`
);
}}
disableTouchRipple
disableRipple
focusRipple={false}
sx={{
color: "#5E5E5E",
border: "#5E5E5E 1px solid",
whiteSpace: "nowrap",
paddingLeft: 4.5,
paddingRight: 4.5,
}}
variant={"outlined"}
>
Manage
</Button>
</Grid>
</Grid>
</Grid>

View File

@@ -35,6 +35,15 @@ export interface IEvent {
reason: string;
}
interface IRequestResource {
cpu: number;
memory: number;
}
export interface IResources {
requests: IRequestResource;
}
export interface IPool {
name: string;
servers: number;
@@ -44,6 +53,7 @@ export interface IPool {
capacity: string;
volumes: number;
label?: string;
resources?: IResources;
}
export interface IPodListElement {
@@ -85,6 +95,7 @@ export interface ITenantStatusUsage {
capacity: number;
capacity_usage: number;
}
export interface ITenantStatus {
write_quorum: string;
drives_online: string;
@@ -101,24 +112,30 @@ export interface ITenantEncryptionResponse {
server: ICertificateInfo[];
client: ICertificateInfo[];
/*
gemalto:
type: object
$ref: "#/definitions/gemaltoConfiguration"
aws:
type: object
$ref: "#/definitions/awsConfiguration"
vault:
type: object
$ref: "#/definitions/vaultConfiguration"
gcp:
type: object
$ref: "#/definitions/gcpConfiguration"
azure:
type: object
$ref: "#/definitions/azureConfiguration"
securityContext:
type: object
$ref: "#/definitions/securityContext"*/
gemalto:
type: object
$ref: "#/definitions/gemaltoConfiguration"
aws:
type: object
$ref: "#/definitions/awsConfiguration"
vault:
type: object
$ref: "#/definitions/vaultConfiguration"
gcp:
type: object
$ref: "#/definitions/gcpConfiguration"
azure:
type: object
$ref: "#/definitions/azureConfiguration"
securityContext:
type: object
$ref: "#/definitions/securityContext"*/
}
export interface ITenantTier {
name: string;
type: string;
size: number;
}
export interface ITenant {
@@ -129,7 +146,7 @@ export interface ITenant {
pool_count: number;
currentState: string;
instance_count: 4;
creation_date: Date;
creation_date: string;
volume_size: number;
volume_count: number;
volumes_per_server: number;
@@ -149,6 +166,7 @@ export interface ITenant {
capacity_raw_usage?: number;
capacity?: number;
capacity_usage?: number;
tiers?: ITenantTier[];
// computed
total_capacity: string;
subnet_license: SubnetInfo;
@@ -224,3 +242,14 @@ export interface ValueUnit {
value: string;
unit: string;
}
export interface CapacityValues {
value: number;
variant: string;
}
export interface CapacityValue {
value: number;
label: string;
color: string;
}

View File

@@ -0,0 +1,140 @@
// 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, { Fragment } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
containerForHeader,
spacingUtils,
tableStyles,
tenantDetailsStyles,
textStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../../../actions";
import { AppState } from "../../../../../../store";
import { setTenantDetailsLoad } from "../../../actions";
import { Box } from "@mui/material";
import { ITenant } from "../../../ListTenants/types";
import Grid from "@mui/material/Grid";
import LabelValuePair from "../../../../Common/UsageBarWrapper/LabelValuePair";
import { niceBytesInt } from "../../../../../../common/utils";
interface IPoolDetails {
classes: any;
loadingTenant: boolean;
tenant: ITenant | null;
selectedPool: string | null;
closeDetailsView: () => void;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
const styles = (theme: Theme) =>
createStyles({
...spacingUtils,
...textStyleUtils,
...tenantDetailsStyles,
...tableStyles,
...containerForHeader(theme.spacing(4)),
});
const twoColCssGridLayoutConfig = {
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
};
const PoolDetails = ({
closeDetailsView,
tenant,
selectedPool,
}: IPoolDetails) => {
const poolInformation =
tenant?.pools.find((pool) => pool.name === selectedPool) || null;
if (poolInformation === null) {
return null;
}
return (
<Fragment>
<Grid item xs={12}>
<Grid container>
<Grid item xs={8}>
<h4>Pool Configuration</h4>
</Grid>
<Grid item xs={4} />
</Grid>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<LabelValuePair label={"Pool Name"} value={poolInformation.label} />
<LabelValuePair
label={"Total Volumes"}
value={poolInformation.volumes}
/>
<LabelValuePair
label={"Volumes per server"}
value={poolInformation.volumes_per_server}
/>
<LabelValuePair label={"Capacity"} value={poolInformation.capacity} />
</Box>
<Grid container>
<Grid item xs={8}>
<h4>Resources</h4>
</Grid>
<Grid item xs={4} />
</Grid>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
{poolInformation.resources && (
<Fragment>
<LabelValuePair
label={"CPU"}
value={poolInformation.resources.requests.cpu}
/>
<LabelValuePair
label={"Memory"}
value={niceBytesInt(poolInformation.resources.requests.memory)}
/>
</Fragment>
)}
<LabelValuePair
label={"Volume Size"}
value={niceBytesInt(poolInformation.volume_configuration.size)}
/>
<LabelValuePair
label={"Storage Class Name"}
value={poolInformation.volume_configuration.storage_class_name}
/>
</Box>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
selectedTenant: state.tenants.tenantDetails.currentTenant,
tenant: state.tenants.tenantDetails.tenantInfo,
selectedPool: state.tenants.tenantDetails.selectedPool,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setTenantDetailsLoad,
});
export default withStyles(styles)(connector(PoolDetails));

View File

@@ -0,0 +1,166 @@
// 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, { Fragment, useEffect, useState } from "react";
import withStyles from "@mui/styles/withStyles";
import { AppState } from "../../../../../../store";
import { connect } from "react-redux";
import { setErrorSnackMessage } from "../../../../../../actions";
import { setSelectedPool } from "../../../actions";
import { IPool, ITenant } from "../../../ListTenants/types";
import Grid from "@mui/material/Grid";
import { TextField } from "@mui/material";
import InputAdornment from "@mui/material/InputAdornment";
import SearchIcon from "../../../../../../icons/SearchIcon";
import RBIconButton from "../../../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { AddIcon } from "../../../../../../icons";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import {
actionsTray,
containerForHeader,
tableStyles,
tenantDetailsStyles,
} from "../../../../Common/FormComponents/common/styleLibrary";
interface IPoolsSummary {
classes: any;
tenant: ITenant | null;
loadingTenant: boolean;
history: any;
setPoolDetailsView: () => void;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSelectedPool: typeof setSelectedPool;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...actionsTray,
...tableStyles,
...containerForHeader(theme.spacing(4)),
});
const PoolsListing = ({
classes,
tenant,
loadingTenant,
setSelectedPool,
history,
setPoolDetailsView,
}: IPoolsSummary) => {
const [pools, setPools] = useState<IPool[]>([]);
const [filter, setFilter] = useState<string>("");
useEffect(() => {
if (tenant) {
const resPools = !tenant.pools ? [] : tenant.pools;
setPools(resPools);
}
}, [tenant]);
const filteredPools = pools.filter((pool) => {
if (pool.name.toLowerCase().includes(filter.toLowerCase())) {
return true;
}
return false;
});
const listActions = [
{
type: "view",
onClick: (selectedValue: IPool) => {
setSelectedPool(selectedValue.name);
setPoolDetailsView();
},
},
];
return (
<Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
variant="standard"
/>
<RBIconButton
tooltip={"Expand Tenant"}
text={"Expand Tenant"}
onClick={() => {
history.push(
`/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/add-pool`
);
}}
icon={<AddIcon />}
color="primary"
variant={"contained"}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={listActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={loadingTenant}
records={filteredPools}
entityName="Servers"
idField="name"
customEmptyMessage="No Pools found"
/>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
selectedTenant: state.tenants.tenantDetails.currentTenant,
tenant: state.tenants.tenantDetails.tenantInfo,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setSelectedPool,
});
export default withStyles(styles)(connector(PoolsListing));

View File

@@ -14,7 +14,7 @@
// 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, useEffect, useState } from "react";
import React, { Fragment, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@@ -25,25 +25,23 @@ import {
tableStyles,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { TextField } from "@mui/material";
import Grid from "@mui/material/Grid";
import { AddIcon } from "../../../../icons";
import { IPool, ITenant } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import InputAdornment from "@mui/material/InputAdornment";
import { AppState } from "../../../../store";
import { setTenantDetailsLoad } from "../actions";
import SearchIcon from "../../../../icons/SearchIcon";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { setSelectedPool, setTenantDetailsLoad } from "../actions";
import PoolsListing from "./Pools/Details/PoolsListing";
import PoolDetails from "./Pools/Details/PoolDetails";
import BackLink from "../../../../common/BackLink";
interface IPoolsSummary {
classes: any;
tenant: ITenant | null;
loadingTenant: boolean;
history: any;
match: any;
selectedPool: string | null;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
setSelectedPool: typeof setSelectedPool;
}
const styles = (theme: Theme) =>
@@ -56,88 +54,42 @@ const styles = (theme: Theme) =>
const PoolsSummary = ({
classes,
tenant,
loadingTenant,
setTenantDetailsLoad,
history,
selectedPool,
match,
}: IPoolsSummary) => {
const [pools, setPools] = useState<IPool[]>([]);
const [filter, setFilter] = useState<string>("");
useEffect(() => {
if (tenant) {
const resPools = !tenant.pools ? [] : tenant.pools;
setPools(resPools);
}
}, [tenant]);
const filteredPools = pools.filter((pool) => {
if (pool.name.toLowerCase().includes(filter.toLowerCase())) {
return true;
}
return false;
});
const [poolDetailsOpen, setPoolDetailsOpen] = useState<boolean>(false);
return (
<Fragment>
<h1 className={classes.sectionTitle}>Pools</h1>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter"
className={classes.searchField}
id="search-resource"
label=""
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
variant="standard"
/>
<RBIconButton
tooltip={"Expand Tenant"}
text={"Expand Tenant"}
onClick={() => {
history.push(
`/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/add-pool`
);
}}
icon={<AddIcon />}
color="primary"
variant={"contained"}
/>
</Grid>
{poolDetailsOpen && (
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={[]}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={loadingTenant}
records={filteredPools}
entityName="Servers"
idField="name"
customEmptyMessage="No Pools found"
<BackLink
executeOnClick={() => {
setPoolDetailsOpen(false);
}}
label={"Back to Pools list"}
to={match.url}
/>
</Grid>
)}
<h1 className={classes.sectionTitle}>
{poolDetailsOpen ? `Pool Details - ${selectedPool || ""}` : "Pools"}
</h1>
<Grid container>
{poolDetailsOpen ? (
<PoolDetails
closeDetailsView={() => {
setPoolDetailsOpen(false);
}}
/>
) : (
<PoolsListing
setPoolDetailsView={() => {
setPoolDetailsOpen(true);
}}
history={history}
/>
)}
</Grid>
</Fragment>
);
@@ -146,12 +98,14 @@ const PoolsSummary = ({
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
selectedTenant: state.tenants.tenantDetails.currentTenant,
selectedPool: state.tenants.tenantDetails.selectedPool,
tenant: state.tenants.tenantDetails.tenantInfo,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setTenantDetailsLoad,
setSelectedPool,
});
export default withStyles(styles)(connector(PoolsSummary));

View File

@@ -58,6 +58,7 @@ import {
ADD_POOL_ADD_NEW_TOLERATION,
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_KEY_PAIR_VALUE,
POOL_DETAILS_SET_SELECTED_POOL,
} from "./types";
import { ITolerationModel } from "../../../common/types";
@@ -407,3 +408,10 @@ export const setPoolKeyValuePairs = (newArray: LabelKeyPair[]) => {
newArray,
};
};
export const setSelectedPool = (newPool: string | null) => {
return {
type: POOL_DETAILS_SET_SELECTED_POOL,
pool: newPool,
};
};

View File

@@ -57,6 +57,7 @@ import {
ADD_POOL_SET_TOLERATION_VALUE,
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_KEY_PAIR_VALUE,
POOL_DETAILS_SET_SELECTED_POOL,
} from "./types";
import { KeyPair } from "./ListTenants/utils";
import { getRandomString } from "./utils";
@@ -363,6 +364,7 @@ const initialState: ITenantState = {
loadingTenant: false,
tenantInfo: null,
currentTab: "summary",
selectedPool: null,
},
addPool: {
addPoolLoading: false,
@@ -1163,6 +1165,14 @@ export function tenantsReducer(
},
},
};
case POOL_DETAILS_SET_SELECTED_POOL:
return {
...state,
tenantDetails: {
...state.tenantDetails,
selectedPool: action.pool,
},
};
case ADD_POOL_RESET_FORM:
return {
...state,

View File

@@ -98,6 +98,9 @@ export const ADD_POOL_SET_TOLERATION_VALUE = "ADD_POOL/SET_TOLERATION_VALUE";
export const ADD_POOL_ADD_NEW_TOLERATION = "ADD_POOL/ADD_NEW_TOLERATION";
export const ADD_POOL_REMOVE_TOLERATION_ROW = "ADD_POOL/REMOVE_TOLERATION_ROW";
// Pool Details
export const POOL_DETAILS_SET_SELECTED_POOL = "POOL_DETAILS/SET_SELECTED_POOL";
export interface ICertificateInfo {
name: string;
serialNumber: string;
@@ -364,6 +367,7 @@ export interface ITenantDetails {
loadingTenant: boolean;
tenantInfo: ITenant | null;
currentTab: string;
selectedPool: string | null;
}
export interface ITenantState {
@@ -635,6 +639,11 @@ interface SetPoolSelectorKeyPairValueArray {
newArray: LabelKeyPair[];
}
interface SetSelectedPool {
type: typeof POOL_DETAILS_SET_SELECTED_POOL;
pool: string | null;
}
export type FieldsToHandle = INameTenantFields;
export type TenantsManagementTypes =
@@ -676,4 +685,5 @@ export type TenantsManagementTypes =
| SetPoolTolerationValue
| AddNewPoolToleration
| RemovePoolTolerationRow
| SetPoolSelectorKeyPairValueArray;
| SetPoolSelectorKeyPairValueArray
| SetSelectedPool;

View File

@@ -1420,6 +1420,10 @@ definitions:
capacity_usage:
type: integer
format: int64
tiers:
type: array
items:
$ref: "#/definitions/tenantTierElement"
listTenantsResponse:
type: object
@@ -2857,3 +2861,14 @@ definitions:
type: string
latest_version:
type: string
tenantTierElement:
type: object
properties:
name:
type: string
type:
type: string
size:
type: integer
format: int64