New tenants list (#1160)

* New Tenants Listing

* Removed all warnings and duplciate comments
This commit is contained in:
Daniel Valdivia
2021-10-28 10:29:38 -07:00
committed by GitHub
parent 610c8a4653
commit 7a5cc660d4
18 changed files with 735 additions and 259 deletions

2
go.sum
View File

@@ -911,8 +911,6 @@ github.com/minio/kes v0.11.0/go.mod h1:mTF1Bv8YVEtQqF/B7Felp4tLee44Pp+dgI0rhCvgN
github.com/minio/madmin-go v1.0.12/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs=
github.com/minio/madmin-go v1.1.10 h1:pfMgXkzdwADnNfVdNMJbwok2fjb2sJ7Q76kDt89RGzE=
github.com/minio/madmin-go v1.1.10/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
github.com/minio/mc v0.0.0-20211026035615-633978474384 h1:kegqQrdBZ6Vzpa6WI+dop2j7ysRIXNa1ZOC08vSDgNE=
github.com/minio/mc v0.0.0-20211026035615-633978474384/go.mod h1:vxztwXLB9Gyl/h3Yh08Mpz1CB/0FO5Es0iQRpzxvS5I=
github.com/minio/mc v0.0.0-20211027024940-7866f97ef502 h1:7ip9qTspUniv+WDENgOcfUr95IccxG5aDkBM4Z96kQg=
github.com/minio/mc v0.0.0-20211027024940-7866f97ef502/go.mod h1:vxztwXLB9Gyl/h3Yh08Mpz1CB/0FO5Es0iQRpzxvS5I=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=

View File

@@ -34,6 +34,18 @@ import (
// swagger:model tenantList
type TenantList struct {
// capacity
Capacity int64 `json:"capacity,omitempty"`
// capacity raw
CapacityRaw int64 `json:"capacity_raw,omitempty"`
// capacity raw usage
CapacityRawUsage int64 `json:"capacity_raw_usage,omitempty"`
// capacity usage
CapacityUsage int64 `json:"capacity_usage,omitempty"`
// creation date
CreationDate string `json:"creation_date,omitempty"`

View File

@@ -25,6 +25,7 @@ package models
import (
"context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@@ -46,17 +47,69 @@ type TenantStatus struct {
// health status
HealthStatus string `json:"health_status,omitempty"`
// usage
Usage *TenantStatusUsage `json:"usage,omitempty"`
// write quorum
WriteQuorum int32 `json:"write_quorum,omitempty"`
}
// Validate validates this tenant status
func (m *TenantStatus) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateUsage(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContextValidate validates this tenant status based on context it is used
func (m *TenantStatus) validateUsage(formats strfmt.Registry) error {
if swag.IsZero(m.Usage) { // not required
return nil
}
if m.Usage != nil {
if err := m.Usage.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("usage")
}
return err
}
}
return nil
}
// ContextValidate validate this tenant status based on the context it is used
func (m *TenantStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateUsage(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *TenantStatus) contextValidateUsage(ctx context.Context, formats strfmt.Registry) error {
if m.Usage != nil {
if err := m.Usage.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("usage")
}
return err
}
}
return nil
}
@@ -77,3 +130,49 @@ func (m *TenantStatus) UnmarshalBinary(b []byte) error {
*m = res
return nil
}
// TenantStatusUsage tenant status usage
//
// swagger:model TenantStatusUsage
type TenantStatusUsage struct {
// capacity
Capacity int64 `json:"capacity,omitempty"`
// capacity usage
CapacityUsage int64 `json:"capacity_usage,omitempty"`
// raw
Raw int64 `json:"raw,omitempty"`
// raw usage
RawUsage int64 `json:"raw_usage,omitempty"`
}
// Validate validates this tenant status usage
func (m *TenantStatusUsage) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this tenant status usage based on context it is used
func (m *TenantStatusUsage) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *TenantStatusUsage) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *TenantStatusUsage) UnmarshalBinary(b []byte) error {
var res TenantStatusUsage
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -379,7 +379,7 @@ func init() {
"tags": [
"UserAPI"
],
"summary": "Logout from Console.",
"summary": "Logout from Operator.",
"operationId": "Logout",
"responses": {
"200": {
@@ -2895,6 +2895,22 @@ func init() {
"tenantList": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"format": "int64"
},
"capacity_raw": {
"type": "integer",
"format": "int64"
},
"capacity_raw_usage": {
"type": "integer",
"format": "int64"
},
"capacity_usage": {
"type": "integer",
"format": "int64"
},
"creation_date": {
"type": "string"
},
@@ -3007,6 +3023,27 @@ func init() {
"health_status": {
"type": "string"
},
"usage": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"format": "int64"
},
"capacity_usage": {
"type": "integer",
"format": "int64"
},
"raw": {
"type": "integer",
"format": "int64"
},
"raw_usage": {
"type": "integer",
"format": "int64"
}
}
},
"write_quorum": {
"type": "integer",
"format": "int32"
@@ -3526,7 +3563,7 @@ func init() {
"tags": [
"UserAPI"
],
"summary": "Logout from Console.",
"summary": "Logout from Operator.",
"operationId": "Logout",
"responses": {
"200": {
@@ -5203,6 +5240,27 @@ func init() {
}
}
},
"TenantStatusUsage": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"format": "int64"
},
"capacity_usage": {
"type": "integer",
"format": "int64"
},
"raw": {
"type": "integer",
"format": "int64"
},
"raw_usage": {
"type": "integer",
"format": "int64"
}
}
},
"UpdateTenantSecurityRequestCustomCertificates": {
"type": "object",
"properties": {
@@ -6599,6 +6657,22 @@ func init() {
"tenantList": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"format": "int64"
},
"capacity_raw": {
"type": "integer",
"format": "int64"
},
"capacity_raw_usage": {
"type": "integer",
"format": "int64"
},
"capacity_usage": {
"type": "integer",
"format": "int64"
},
"creation_date": {
"type": "string"
},
@@ -6711,6 +6785,27 @@ func init() {
"health_status": {
"type": "string"
},
"usage": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"format": "int64"
},
"capacity_usage": {
"type": "integer",
"format": "int64"
},
"raw": {
"type": "integer",
"format": "int64"
},
"raw_usage": {
"type": "integer",
"format": "int64"
}
}
},
"write_quorum": {
"type": "integer",
"format": "int32"

View File

@@ -50,7 +50,7 @@ func NewLogout(ctx *middleware.Context, handler LogoutHandler) *Logout {
/* Logout swagger:route POST /logout UserAPI logout
Logout from Console.
Logout from Operator.
*/
type Logout struct {

View File

@@ -526,6 +526,12 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
DrivesOffline: minTenant.Status.DrivesOffline,
DrivesOnline: minTenant.Status.DrivesOnline,
WriteQuorum: minTenant.Status.WriteQuorum,
Usage: &models.TenantStatusUsage{
Raw: minTenant.Status.Usage.RawCapacity,
RawUsage: minTenant.Status.Usage.RawUsage,
Capacity: minTenant.Status.Usage.Capacity,
CapacityUsage: minTenant.Status.Usage.Usage,
},
}
// get tenant service
@@ -824,16 +830,20 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
}
tenants = append(tenants, &models.TenantList{
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
Name: tenant.ObjectMeta.Name,
PoolCount: int64(len(tenant.Spec.Pools)),
InstanceCount: instanceCount,
VolumeCount: volumeCount,
CurrentState: tenant.Status.CurrentState,
Namespace: tenant.ObjectMeta.Namespace,
TotalSize: totalSize,
HealthStatus: string(tenant.Status.HealthStatus),
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
Name: tenant.ObjectMeta.Name,
PoolCount: int64(len(tenant.Spec.Pools)),
InstanceCount: instanceCount,
VolumeCount: volumeCount,
CurrentState: tenant.Status.CurrentState,
Namespace: tenant.ObjectMeta.Namespace,
TotalSize: totalSize,
HealthStatus: string(tenant.Status.HealthStatus),
CapacityRaw: tenant.Status.Usage.RawCapacity,
CapacityRawUsage: tenant.Status.Usage.RawUsage,
Capacity: tenant.Status.Usage.Capacity,
CapacityUsage: tenant.Status.Usage.Usage,
})
}

View File

@@ -67,6 +67,13 @@ const GlobalCss = withStyles({
borderRadius: 0,
},
},
hr: {
borderTop: 0,
borderLeft: 0,
borderRight: 0,
borderColor: "#999999",
backgroundColor: "transparent" as const,
},
},
})(() => null);

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
@@ -15,6 +15,7 @@ interface IProgressBar {
error: string;
loading: boolean;
classes: any;
labels?: boolean;
}
const styles = (theme: Theme) =>
@@ -36,7 +37,7 @@ const styles = (theme: Theme) =>
},
});
const BorderLinearProgress = withStyles((theme) => ({
export const BorderLinearProgress = withStyles((theme) => ({
root: {
height: 10,
borderRadius: 5,
@@ -61,6 +62,7 @@ const UsageBarWrapper = ({
renderFunction,
loading,
error,
labels = true,
}: IProgressBar) => {
const porcentualValue = (currValue * 100) / maxValue;
@@ -71,13 +73,25 @@ const UsageBarWrapper = ({
) : (
<React.Fragment>
<Grid item xs={12} className={classes.allValue}>
{label}{" "}
{renderFunction ? renderFunction(maxValue.toString()) : maxValue}
{labels && (
<Fragment>
{label}{" "}
{renderFunction
? renderFunction(maxValue.toString())
: maxValue}
</Fragment>
)}
</Grid>
<BorderLinearProgress variant="determinate" value={porcentualValue} />
<Grid item xs={12} className={classes.currentUsage}>
Used:{" "}
{renderFunction ? renderFunction(currValue.toString()) : currValue}
{labels && (
<Fragment>
Used:{" "}
{renderFunction
? renderFunction(currValue.toString())
: currValue}
</Fragment>
)}
</Grid>
</React.Fragment>
);

View File

@@ -44,7 +44,6 @@ import Account from "./Account/Account";
import Users from "./Users/Users";
import Groups from "./Groups/Groups";
import ConfigurationMain from "./Configurations/ConfigurationMain";
import TenantsMain from "./Tenants/TenantsMain";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
import License from "./License/License";
import Trace from "./Trace/Trace";
@@ -63,6 +62,7 @@ import NotificationTypeSelector from "./NotificationEndpoints/NotificationTypeSe
import ListTiersConfiguration from "./Configurations/TiersConfiguration/ListTiersConfiguration";
import TierTypeSelector from "./Configurations/TiersConfiguration/TierTypeSelector";
import AddTierConfiguration from "./Configurations/TiersConfiguration/AddTierConfiguration";
import ListTenants from "./Tenants/ListTenants/ListTenants";
const drawerWidth = 245;
@@ -321,7 +321,7 @@ const Console = ({
},
},
{
component: TenantsMain,
component: ListTenants,
path: "/tenants",
},
{

View File

@@ -19,7 +19,7 @@ import { connect } from "react-redux";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import { IconButton } from "@mui/material";
import { Box, Button, IconButton } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
@@ -28,18 +28,19 @@ import { niceBytes } from "../../../../common/utils";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import {
actionsTray,
containerForHeader,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import { CircleIcon, CreateIcon } from "../../../../icons";
import { AddIcon } from "../../../../icons";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import DeleteTenant from "./DeleteTenant";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import RefreshIcon from "../../../../icons/RefreshIcon";
import SearchIcon from "../../../../icons/SearchIcon";
import PageHeader from "../../Common/PageHeader/PageHeader";
import TenantListItem from "./TenantListItem";
interface ITenantsList {
classes: any;
@@ -50,48 +51,61 @@ const styles = (theme: Theme) =>
createStyles({
...actionsTray,
...searchField,
redState: {
color: theme.palette.error.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
...containerForHeader(theme.spacing(4)),
addTenant: {
marginRight: 8,
},
yellowState: {
color: theme.palette.warning.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
theaderSearchLabel: {
color: theme.palette.grey["400"],
fontSize: 14,
fontWeight: "bold",
},
greenState: {
color: theme.palette.success.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
addBucket: {
marginRight: 8,
},
greyState: {
color: "grey",
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
theaderSearch: {
borderColor: theme.palette.grey["200"],
"& .MuiInputBase-input": {
paddingTop: 10,
paddingBottom: 10,
},
"& .MuiInputBase-root": {
"& .MuiInputAdornment-root": {
"& .MuiSvgIcon-root": {
color: theme.palette.grey["400"],
height: 14,
},
},
},
actionHeaderItems: {
"@media (min-width: 320px)": {
marginTop: 8,
},
},
marginRight: 10,
marginLeft: 10,
},
mainActions: {
textAlign: "right",
},
healthStatusIcon: {
position: "relative",
fontSize: 10,
right: -30,
height: 10,
top: -50,
},
tenantItem: {
border: "1px solid #dedede",
marginBottom: 20,
paddingLeft: 40,
paddingRight: 40,
paddingTop: 30,
paddingBottom: 30,
},
});
const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterTenants, setFilterTenants] = useState<string>("");
const [records, setRecords] = useState<any[]>([]);
@@ -99,34 +113,11 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
const [createdAccount, setCreatedAccount] =
useState<NewServiceAccount | null>(null);
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const confirmDeleteTenant = (tenant: ITenant) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
const redirectToTenantDetails = (tenant: ITenant) => {
history.push(`/namespaces/${tenant.namespace}/tenants/${tenant.name}`);
return;
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setCreatedAccount(null);
};
const tableActions = [
{ type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant },
];
const filteredRecords = records.filter((b: any) => {
if (filterTenants === "") {
return true;
@@ -155,7 +146,9 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
}
for (let i = 0; i < resTenants.length; i++) {
resTenants[i].capacity = niceBytes(resTenants[i].total_size + "");
resTenants[i].total_capacity = niceBytes(
resTenants[i].total_size + ""
);
}
setRecords(resTenants);
@@ -174,28 +167,8 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
setIsLoading(true);
}, []);
const healthStatusToClass = (health_status: string) => {
switch (health_status) {
case "red":
return classes.redState;
case "yellow":
return classes.yellowState;
case "green":
return classes.greenState;
default:
return classes.greyState;
}
};
return (
<Fragment>
{deleteOpen && (
<DeleteTenant
deleteOpen={deleteOpen}
selectedTenant={selectedTenant}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={createdAccount}
@@ -206,78 +179,101 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
entity="Tenant"
/>
)}
<PageHeader
label="Tenants"
actions={
<Fragment>
<Grid
container
direction="row"
justifyContent="flex-end"
alignItems="center"
className={classes.actionHeaderItems}
>
<Box display={{ xs: "none", sm: "none", md: "block" }}>
<Grid item>
<div className={classes.theaderSearchLabel}>
Filter Tenants:
</div>
</Grid>
</Box>
<Box display={{ xs: "block", sm: "block", md: "none" }}>
<TextField
className={classes.theaderSearch}
variant={"outlined"}
id="search-resource"
placeholder={"Filter Tenants"}
onChange={(val) => {
setFilterTenants(val.target.value);
}}
inputProps={{
disableUnderline: true,
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
<Box display={{ xs: "none", sm: "none", md: "block" }}>
<TextField
className={classes.theaderSearch}
variant={"outlined"}
id="search-resource"
onChange={(val) => {
setFilterTenants(val.target.value);
}}
inputProps={{
disableUnderline: true,
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
<Grid item>
<Button
variant="contained"
color="primary"
endIcon={<AddIcon />}
onClick={() => {
history.push("/tenants/add");
}}
className={classes.addTenant}
>
Create Tenant
</Button>
</Grid>
</Grid>
</Fragment>
}
/>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
variant="standard"
/>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
size="large"
>
<RefreshIcon />
</IconButton>
<IconButton
color="primary"
aria-label="Create Tenant"
component="span"
onClick={() => {
history.push("/tenants/add");
}}
size="large"
>
<CreateIcon />
</IconButton>
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFullObject: true,
renderFunction: (t) => {
return (
<React.Fragment>
<div className={healthStatusToClass(t.health_status)}>
<CircleIcon />
</div>
<div>{t.name}</div>
</React.Fragment>
);
},
},
{ label: "Namespace", elementKey: "namespace" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Pools", elementKey: "pool_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
/>
<Grid item xs={12} className={classes.container}>
<Grid container>
<Grid item xs={12} className={classes.mainActions}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
size="large"
>
<RefreshIcon />
</IconButton>
</Grid>
<Grid item xs={12}>
{filteredRecords.map((t) => {
return <TenantListItem tenant={t} />;
})}
</Grid>
</Grid>
</Grid>
</Grid>
</Fragment>

View File

@@ -0,0 +1,270 @@
// 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 { Button } from "@mui/material";
import { ITenant } from "./types";
import { connect } from "react-redux";
import { setErrorSnackMessage } from "../../../../actions";
import Grid from "@mui/material/Grid";
import { ArrowRightIcon, CircleIcon } 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";
const styles = (theme: Theme) =>
createStyles({
redState: {
color: theme.palette.error.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
},
yellowState: {
color: theme.palette.warning.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
},
greenState: {
color: theme.palette.success.main,
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
},
greyState: {
color: "grey",
"& .MuiSvgIcon-root": {
width: 16,
height: 16,
float: "left",
marginRight: 4,
},
},
tenantIcon: { width: 40, height: 40, position: "relative" },
healthStatusIcon: {
position: "absolute",
fontSize: 10,
top: 0,
right: -20,
height: 10,
},
tenantItem: {
border: "1px solid #dedede",
marginBottom: 20,
paddingLeft: 40,
paddingRight: 40,
paddingTop: 30,
paddingBottom: 30,
},
title: {
fontSize: 22,
fontWeight: "bold",
},
titleSubKey: {
fontSize: 14,
paddingRight: 8,
},
titleSubValue: {
fontSize: 14,
fontWeight: "bold",
paddingRight: 16,
},
boxyTitle: {
fontWeight: "bold",
},
boxyValue: {
fontSize: 24,
fontWeight: "bold",
},
boxyUnit: {
fontSize: 12,
color: "#5E5E5E",
},
});
interface ITenantListItem {
tenant: ITenant;
classes: any;
}
interface ValueUnit {
value: string;
unit: string;
}
const TenantListItem = ({ tenant, classes }: ITenantListItem) => {
const healthStatusToClass = (health_status: string) => {
switch (health_status) {
case "red":
return classes.redState;
case "yellow":
return classes.yellowState;
case "green":
return classes.greenState;
default:
return classes.greyState;
}
};
var raw: ValueUnit = { value: "n/a", unit: "" };
var capacity: ValueUnit = { value: "n/a", unit: "" };
var used: ValueUnit = { value: "n/a", unit: "" };
if (tenant.capacity_raw) {
const b = niceBytes(`${tenant.capacity_raw}`, true);
const parts = b.split(" ");
raw.value = parts[0];
raw.unit = parts[1];
}
if (tenant.capacity) {
const b = niceBytes(`${tenant.capacity}`, true);
const parts = b.split(" ");
capacity.value = parts[0];
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 parts = b.split(" ");
used.value = parts[0];
used.unit = parts[1];
}
return (
<Fragment>
<div className={classes.tenantItem}>
<Grid container>
<Grid item xs={10}>
<div className={classes.title}>{tenant.name}</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>
</div>
</Grid>
<Grid item xs={2} textAlign={"end"}>
<Button
endIcon={<ArrowRightIcon />}
variant="contained"
onClick={() => {
history.push(
`/namespaces/${tenant.namespace}/tenants/${tenant.name}`
);
}}
>
View
</Button>
</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}>
<div className={classes.healthStatusIcon}>
<span
className={healthStatusToClass(tenant.health_status)}
>
<CircleIcon />
</span>
</div>
<TenantsIcon style={{ fontSize: 40 }} />
</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>
</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>
</Grid>
</Grid>
</Grid>
</div>
</Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(TenantListItem));

View File

@@ -68,12 +68,19 @@ export interface IEndpoints {
console: string;
}
export interface ITenantStatusUsage {
raw: number;
raw_usage: number;
capacity: number;
capacity_usage: number;
}
export interface ITenantStatus {
write_quorum: string;
drives_online: string;
drives_offline: string;
drives_healing: string;
health_status: string;
usage?: ITenantStatusUsage;
}
export interface ITenant {
@@ -100,8 +107,12 @@ export interface ITenant {
idpOidcEnabled: boolean;
health_status: string;
status?: ITenantStatus;
capacity_raw?: number;
capacity_raw_usage?: number;
capacity?: number;
capacity_usage?: number;
// computed
capacity: string;
total_capacity: string;
subnet_license: LicenseInfo;
total_instances?: number;
total_volumes?: number;

View File

@@ -28,12 +28,10 @@ import Grid from "@mui/material/Grid";
import { Button, CircularProgress } from "@mui/material";
import Paper from "@mui/material/Paper";
import { niceBytes } from "../../../../common/utils";
import api from "../../../../common/api";
import { ITenant } from "../ListTenants/types";
import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper";
import UpdateTenantModal from "./UpdateTenantModal";
import { AppState } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import history from "./../../../../history";
import { CircleIcon } from "../../../../icons";
@@ -52,11 +50,6 @@ interface ITenantsSummary {
loadingTenant: boolean;
}
interface ITenantUsage {
used: string;
disk_used: string;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
@@ -136,9 +129,6 @@ const TenantSummary = ({
const [poolCount, setPoolCount] = useState<number>(0);
const [instances, setInstances] = useState<number>(0);
const [volumes, setVolumes] = useState<number>(0);
const [loadingUsage, setLoadingUsage] = useState<boolean>(true);
const [usageError, setUsageError] = useState<string>("");
const [usage, setUsage] = useState<number>(0);
const [updateMinioVersion, setUpdateMinioVersion] = useState<boolean>(false);
const tenantName = match.params["tenantName"];
@@ -154,27 +144,6 @@ const TenantSummary = ({
: classes.greyState;
};
useEffect(() => {
if (loadingUsage) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/usage`
)
.then((result: ITenantUsage) => {
const usage = get(result, "disk_used", "0");
setUsage(parseInt(usage));
setUsageError("");
setLoadingUsage(false);
})
.catch((err: ErrorResponseHandler) => {
setUsageError(err.errorMessage);
setUsage(0);
setLoadingUsage(false);
});
}
}, [tenantName, tenantNamespace, loadingUsage]);
useEffect(() => {
if (tenant) {
setCapacity(tenant.total_size || 0);
@@ -302,12 +271,12 @@ const TenantSummary = ({
) : (
<Fragment>
<UsageBarWrapper
currValue={usage}
maxValue={tenant ? tenant.total_size : 0}
currValue={tenant?.status?.usage?.raw_usage ?? 0}
maxValue={tenant?.status?.usage?.raw ?? 1}
label={"Storage"}
renderFunction={niceBytes}
error={usageError}
loading={loadingUsage}
error={""}
loading={false}
/>
<h4>
{tenant && tenant.status && (

View File

@@ -1,38 +0,0 @@
import React, { Fragment } from "react";
import PageHeader from "../Common/PageHeader/PageHeader";
import { Grid } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import ListTenants from "./ListTenants/ListTenants";
interface IConfigurationMain {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
headerLabel: {
fontSize: 22,
fontWeight: 600,
color: "#000",
marginTop: 4,
},
...containerForHeader(theme.spacing(4)),
});
const TenantsMain = ({ classes }: IConfigurationMain) => {
return (
<Fragment>
<PageHeader label="Tenants" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<ListTenants />
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(TenantsMain);

View File

@@ -14,11 +14,6 @@ const theme = createTheme({
dark: "#ba000d",
contrastText: "#000",
},
error: {
light: "#e03a48",
main: "#dc1f2e",
contrastText: "#fff",
},
grey: {
100: "#f0f0f0",
200: "#e6e6e6",
@@ -33,6 +28,17 @@ const theme = createTheme({
background: {
default: "#fff",
},
success: {
main: "#4ccb92",
},
warning: {
main: "#FFBD62",
},
error: {
light: "#e03a48",
main: "#C83B51",
contrastText: "#fff",
},
},
typography: {
fontFamily: ["Lato", "sans-serif"].join(","),

View File

@@ -30,7 +30,7 @@ const newTheme = createTheme(
default: "#F4F4F4",
},
success: {
main: "#32c787",
main: "#4ccb92",
},
warning: {
main: "#ffb300",

View File

@@ -108,7 +108,7 @@ paths:
/logout:
post:
summary: Logout from Operator.
summary: Logout from Console.
operationId: Logout
responses:
200:

View File

@@ -108,7 +108,7 @@ paths:
/logout:
post:
summary: Logout from Console.
summary: Logout from Operator.
operationId: Logout
responses:
200:
@@ -1040,6 +1040,21 @@ definitions:
format: int32
health_status:
type: string
usage:
type: object
properties:
raw:
type: integer
format: int64
raw_usage:
type: integer
format: int64
capacity:
type: integer
format: int64
capacity_usage:
type: integer
format: int64
tenantSecurityResponse:
type: object
properties:
@@ -1173,6 +1188,18 @@ definitions:
type: string
health_status:
type: string
capacity_raw:
type: integer
format: int64
capacity_raw_usage:
type: integer
format: int64
capacity:
type: integer
format: int64
capacity_usage:
type: integer
format: int64
listTenantsResponse:
type: object