Added URL navigation to tenant details (#795)

Added URL navigation to tenant details and fixed a couple of warnings

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-06-07 22:23:14 -05:00
committed by GitHub
parent bb0f613f5b
commit c2dc7391e4
17 changed files with 1274 additions and 688 deletions

View File

@@ -29,7 +29,7 @@ import {
import { ChangeUserPasswordRequest } from "../Buckets/types";
import api from "../../../common/api";
import { setModalErrorSnackMessage } from "../../../actions";
import { User, UsersList } from "../Users/types";
import { User } from "../Users/types";
const styles = (theme: Theme) =>
createStyles({
@@ -143,7 +143,7 @@ const ChangeUserPassword = ({
color="primary"
disabled={
loading ||
!(reNewPassword.length > 0 && newPassword == reNewPassword)
!(reNewPassword.length > 0 && newPassword === reNewPassword)
}
>
Save

View File

@@ -593,3 +593,82 @@ export const hrClass = {
backgroundColor: "transparent" as const,
},
};
export const tenantDetailsStyles = {
buttonContainer: {
textAlign: "right" as const,
},
multiContainer: {
display: "flex" as const,
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex" as const,
justifyContent: "space-between" as const,
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid" as const,
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex" as const,
alignItems: "center" as const,
},
"& div:nth-child(odd)": {
justifyContent: "flex-end" as const,
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
updateButton: {
backgroundColor: "transparent" as const,
border: 0,
padding: "0 6px",
cursor: "pointer" as const,
"&:focus, &:active": {
outline: "none",
},
"& svg": {
height: 12,
},
},
poolLabel: {
color: "#666666",
},
titleCol: {
fontWeight: 700,
},
breadcrumLink: {
textDecoration: "none",
color: "black",
},
healthCol: {
fontWeight: 700,
paddingRight: "10px",
},
...modalBasic,
...actionsTray,
...buttonsStyles,
...searchField,
...hrClass,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
};

View File

@@ -358,6 +358,22 @@ const Console = ({
component: PodDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName",
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/summary",
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods",
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pools",
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/license",
},
{
component: License,
path: "/license",

View File

@@ -65,7 +65,7 @@ const LicenseModal = ({ classes, open, closeModal }: ILicenseModalProps) => {
<Grid item className={classes.subnetLicenseKey} xs={12}>
<p>
Copyright &copy; 2007 Free Software Foundation, Inc. &lt;
<a target="_blank" href="https://fsf.org/">
<a target="_blank" href="https://fsf.org/" rel="noreferrer">
https://fsf.org/
</a>
&gt;
@@ -904,7 +904,7 @@ const LicenseModal = ({ classes, open, closeModal }: ILicenseModalProps) => {
school, if any, to sign a &quot;copyright disclaimer&quot; for the
program, if necessary. For more information on this, and how to
apply and follow the GNU AGPL, see &lt;
<a target="_blank" href="https://www.gnu.org/licenses/">
<a target="_blank" href="https://www.gnu.org/licenses/" rel="noreferrer">
https://www.gnu.org/licenses/
</a>
&gt;.

View File

@@ -216,13 +216,16 @@ const ListTenants = ({
};
const healthStatusToClass = (health_status: string) => {
return health_status == "red"
? classes.redState
: health_status == "yellow"
? classes.yellowState
: health_status == "green"
? classes.greenState
: classes.greyState;
switch(health_status) {
case "red":
return classes.redState;
case "yellow":
return classes.yellowState;
case "green":
return classes.greenState;
default:
return classes.greyState;
}
};
return (

View File

@@ -0,0 +1,111 @@
// 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, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { niceDays } from "../../../../common/utils";
import { IPodListElement } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
interface IPodsSummary {
match: any;
history: any;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...containerForHeader(theme.spacing(4)),
});
const PodsSummary = ({ match, history }: IPodsSummary) => {
const [pods, setPods] = useState<IPodListElement[]>([]);
const [loadingPods, setLoadingPods] = useState<boolean>(true);
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
const podViewAction = (pod: IPodListElement) => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}/pods/${pod.name}`
);
return;
};
const podTableActions = [{ type: "view", onClick: podViewAction }];
useEffect(() => {
if (loadingPods) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pods`
)
.then((result: IPodListElement[]) => {
for (let i = 0; i < result.length; i++) {
let currentTime = new Date().getSeconds();
result[i].time = niceDays(
(currentTime - parseInt(result[i].timeCreated)).toString()
);
}
setPods(result);
setLoadingPods(false);
})
.catch((err) => {
setErrorSnackMessage("Error loading pods");
});
}
}, [loadingPods, tenantName, tenantNamespace]);
return (
<Fragment>
<br />
<TableWrapper
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Status", elementKey: "status" },
{ label: "Age", elementKey: "time" },
{ label: "Pod IP", elementKey: "podIP" },
{
label: "Restarts",
elementKey: "restarts",
renderFunction: (input) => {
return input != null ? input : 0;
},
},
{ label: "Node", elementKey: "node" },
]}
isLoading={loadingPods}
records={pods}
itemActions={podTableActions}
entityName="Servers"
idField="name"
/>
</Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(PodsSummary));

View File

@@ -0,0 +1,170 @@
// 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, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { Button, TextField } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { CreateIcon } from "../../../../icons";
import { IPool, ITenant } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddPoolModal from "./AddPoolModal";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { AppState } from "../../../../store";
import { setTenantDetailsLoad } from "../actions";
interface IPoolsSummary {
classes: any;
tenant: ITenant | null;
loadingTenant: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
redState: {
color: theme.palette.error.main,
},
yellowState: {
color: theme.palette.warning.main,
},
greenState: {
color: theme.palette.success.main,
},
greyState: {
color: "grey",
},
...containerForHeader(theme.spacing(4)),
});
const PoolsSummary = ({
classes,
tenant,
loadingTenant,
setTenantDetailsLoad
}: IPoolsSummary) => {
const [pools, setPools] = useState<IPool[]>([]);
const [addPoolOpen, setAddPool] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
useEffect(() => {
if(tenant) {
const resPools = !tenant.pools ? [] : tenant.pools;
setPools(resPools);
}
}, [tenant]);
const onClosePoolAndRefresh = (reload: boolean) => {
setAddPool(false);
if (reload) {
setTenantDetailsLoad(true);
}
};
const filteredPools = pools.filter((pool) => {
if (pool.name.toLowerCase().includes(filter.toLowerCase())) {
return true;
}
return false;
});
return (
<Fragment>
{addPoolOpen && tenant !== null && (
<AddPoolModal
open={addPoolOpen}
onClosePoolAndReload={onClosePoolAndRefresh}
tenant={tenant}
/>
)}
<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>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddPool(true);
}}
>
Expand Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<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"
/>
</Grid>
</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,
setTenantDetailsLoad,
});
export default withStyles(styles)(connector(PoolsSummary));

View File

@@ -16,124 +16,53 @@
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import get from "lodash/get";
import { Link, Redirect, Route, Router, Switch } from "react-router-dom";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
buttonsStyles,
containerForHeader,
hrClass,
modalBasic,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { IconButton, Menu, MenuItem } from "@material-ui/core";
import get from 'lodash/get';
import Grid from "@material-ui/core/Grid";
import {
Button,
IconButton,
Menu,
MenuItem,
TextField,
} from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import Paper from "@material-ui/core/Paper";
import { niceBytes, niceDays } from "../../../../common/utils";
import AddPoolModal from "./AddPoolModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
import api from "../../../../common/api";
import { IPodListElement, IPool, ITenant } from "../ListTenants/types";
import PageHeader from "../../Common/PageHeader/PageHeader";
import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper";
import UpdateTenantModal from "./UpdateTenantModal";
import { LicenseInfo } from "../../License/types";
import { Link } from "react-router-dom";
import { setErrorSnackMessage } from "../../../../actions";
import {
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
setTenantTab,
} from "../actions";
import { ITenant } from "../ListTenants/types";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import PageHeader from "../../Common/PageHeader/PageHeader";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import TenantYAML from "./TenantYAML";
import SubnetLicenseTenant from "./SubnetLicenseTenant";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import history from "../../../../history";
import TenantSummary from "./TenantSummary";
import TenantLicense from "./TenantLicense";
import PoolsSummary from "./PoolsSummary";
import PodsSummary from "./PodsSummary";
import { AppState } from "../../../../store";
interface ITenantDetailsProps {
classes: any;
match: any;
history: any;
loadingTenant: boolean;
currentTab: string;
selectedTenant: string;
selectedNamespace: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
interface ITenantUsage {
used: string;
disk_used: string;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
setTenantName: typeof setTenantName;
setTenantInfo: typeof setTenantInfo;
setTenantTab: typeof setTenantTab;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex",
justifyContent: "space-between",
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid",
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
updateButton: {
backgroundColor: "transparent",
border: 0,
padding: "0 6px",
cursor: "pointer",
"&:focus, &:active": {
outline: "none",
},
"& svg": {
height: 12,
},
},
poolLabel: {
color: "#666666",
},
titleCol: {
fontWeight: "bold",
},
breadcrumLink: {
textDecoration: "none",
color: "black",
},
...tenantDetailsStyles,
redState: {
color: theme.palette.error.main,
},
@@ -146,215 +75,91 @@ const styles = (theme: Theme) =>
greyState: {
color: "grey",
},
healthCol: {
fontWeight: "bold",
paddingRight: "10px",
},
...modalBasic,
...actionsTray,
...buttonsStyles,
...searchField,
...hrClass,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
...containerForHeader(theme.spacing(4)),
});
const TenantDetails = ({
classes,
match,
history,
loadingTenant,
currentTab,
selectedTenant,
selectedNamespace,
setErrorSnackMessage,
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
setTenantTab,
}: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
const [poolCount, setPoolCount] = useState<number>(0);
const [pools, setPools] = useState<IPool[]>([]);
const [pods, setPods] = useState<IPodListElement[]>([]);
const [instances, setInstances] = useState<number>(0);
const [volumes, setVolumes] = useState<number>(0);
const [addPoolOpen, setAddPool] = useState<boolean>(false);
const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false);
const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false);
const [tenant, setTenant] = useState<ITenant | null>(null);
const [loadingUsage, setLoadingUsage] = useState<boolean>(true);
const [usageError, setUsageError] = useState<string>("");
const [usage, setUsage] = useState<number>(0);
const [updateMinioVersion, setUpdateMinioVersion] = useState<boolean>(false);
const [yamlScreenOpen, setYamlScreenOpen] = useState<boolean>(false);
const [licenseInfo, setLicenseInfo] = useState<LicenseInfo>();
const [loadingLicenseInfo, setLoadingLicenseInfo] = useState<boolean>(true);
const [loadingActivateProduct, setLoadingActivateProduct] =
useState<boolean>(false);
const [logEnabled, setLogEnabled] = useState<boolean>(false);
const [monitoringEnabled, setMonitoringEnabled] = useState<boolean>(false);
const [encryptionEnabled, setEncryptionEnabled] = useState<boolean>(false);
const [adEnabled, setAdEnabled] = useState<boolean>(false);
const [oicEnabled, setOicEnabled] = useState<boolean>(false);
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
const onClosePoolAndRefresh = (reload: boolean) => {
setAddPool(false);
if (reload) {
loadInfo();
loadUsage();
}
};
const closeBucketsAndRefresh = () => {
setAddBucketOpen(false);
};
const closeReplicationAndRefresh = (reload: boolean) => {
setAddReplicationOpen(false);
if (reload) {
loadInfo();
loadUsage();
}
};
const activateProduct = (namespace: string, tenant: string) => {
if (loadingActivateProduct) {
return;
}
setLoadingActivateProduct(true);
api
.invoke(
"POST",
`/api/v1/subscription/namespaces/${namespace}/tenants/${tenant}/activate`,
{}
)
.then(() => {
setLoadingActivateProduct(false);
loadInfo();
})
.catch((err) => {
setLoadingActivateProduct(false);
setErrorSnackMessage(err);
});
};
const loadInfo = () => {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}`
)
.then((res: ITenant) => {
const resPools = !res.pools ? [] : res.pools;
let totalInstances = 0;
let totalVolumes = 0;
let poolNamedIndex = 0;
for (let pool of resPools) {
const cap =
pool.volumes_per_server *
pool.servers *
pool.volume_configuration.size;
pool.label = `pool-${poolNamedIndex}`;
if (pool.name === undefined || pool.name === "") {
pool.name = pool.label;
}
pool.capacity = niceBytes(cap + "");
pool.volumes = pool.servers * pool.volumes_per_server;
totalInstances += pool.servers;
totalVolumes += pool.volumes;
poolNamedIndex += 1;
}
setCapacity(res.total_size || 0);
setPoolCount(resPools.length);
setVolumes(totalVolumes);
setInstances(totalInstances);
setPools(resPools);
setLogEnabled(res.logEnabled);
setMonitoringEnabled(res.monitoringEnabled);
setEncryptionEnabled(res.encryptionEnabled);
setAdEnabled(res.idpAdEnabled);
setOicEnabled(res.idpOicEnabled);
setTenant(res);
})
.catch((err) => {
setErrorSnackMessage(err);
});
};
const loadUsage = () => {
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) => {
setUsageError(err);
setUsage(0);
setLoadingUsage(false);
});
};
const loadPods = () => {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pods`
)
.then((result: IPodListElement[]) => {
for (let i = 0; i < result.length; i++) {
let currentTime = new Date().getSeconds();
result[i].time = niceDays(
(currentTime - parseInt(result[i].timeCreated)).toString()
);
}
setPods(result);
})
.catch((err) => {
setErrorSnackMessage("Error loading pods");
});
};
const fetchLicenseInfo = () => {
setLoadingLicenseInfo(true);
api
.invoke("GET", `/api/v1/subscription/info`)
.then((res: LicenseInfo) => {
setLicenseInfo(res);
setLoadingLicenseInfo(false);
})
.catch((err: any) => {
setLoadingLicenseInfo(false);
});
};
useEffect(() => {
loadInfo();
loadUsage();
fetchLicenseInfo();
loadPods();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [anchorEl, setAnchorEl] = React.useState(null);
const podViewAction = (pod: IPodListElement) => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}/pods/${pod.name}`
);
return;
};
const podTableActions = [{ type: "view", onClick: podViewAction }];
useEffect(() => {
if (!loadingTenant) {
if (
tenantName !== selectedTenant ||
tenantNamespace !== selectedNamespace
) {
setTenantName(tenantName, tenantNamespace);
setTenantDetailsLoad(true);
}
}
}, [
loadingTenant,
selectedTenant,
selectedNamespace,
setTenantDetailsLoad,
setTenantInfo,
setTenantName,
tenantName,
tenantNamespace,
]);
useEffect(() => {
if (loadingTenant) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}`
)
.then((res: ITenant) => {
setTenantInfo(res);
setTenantDetailsLoad(false);
})
.catch((err) => {
setErrorSnackMessage(err);
setTenantDetailsLoad(false);
});
}
}, [
loadingTenant,
tenantNamespace,
tenantName,
setTenantInfo,
setTenantDetailsLoad,
setErrorSnackMessage,
]);
useEffect(() => {
const path = get(match, 'path', '/');
const splitSections = path.split('/');
const section=splitSections[splitSections.length - 1];
switch(section) {
case "pools":
case "pods":
case "license":
setTenantTab(section);
break;
default:
setTenantTab('summary');
}
}, [match, setTenantTab]);
const handleTenantMenu = (event: any) => {
setAnchorEl(event.currentTarget);
@@ -367,50 +172,11 @@ const TenantDetails = ({
const closeYAMLModalAndRefresh = () => {
setYamlScreenOpen(false);
loadInfo();
};
const healthStatusToClass = (health_status: string) => {
return health_status == "red"
? classes.redState
: health_status == "yellow"
? classes.yellowState
: health_status == "green"
? classes.greenState
: classes.greyState;
setTenantDetailsLoad(true);
};
return (
<React.Fragment>
{addPoolOpen && tenant !== null && (
<AddPoolModal
open={addPoolOpen}
onClosePoolAndReload={onClosePoolAndRefresh}
tenant={tenant}
/>
)}
{addBucketOpen && (
<AddBucket
open={addBucketOpen}
closeModalAndRefresh={closeBucketsAndRefresh}
/>
)}
{addReplicationOpen && (
<ReplicationSetup
open={addReplicationOpen}
closeModalAndRefresh={closeReplicationAndRefresh}
/>
)}
{updateMinioVersion && (
<UpdateTenantModal
open={updateMinioVersion}
closeModalAndRefresh={() => {
setUpdateMinioVersion(false);
}}
idTenant={tenantName}
namespace={tenantNamespace}
/>
)}
<Fragment>
{yamlScreenOpen && (
<TenantYAML
open={yamlScreenOpen}
@@ -452,347 +218,73 @@ const TenantDetails = ({
<Grid container>
<Grid item xs={12}>
<Tabs
value={selectedTab}
value={currentTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
onChange={(_, newValue: string) => {
setTenantTab(newValue);
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}/${newValue}`
);
}}
aria-label="tenant-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab label="Summary" />
<Tab label="Pools" />
<Tab label="Pods" />
<Tab label="License" />
<Tab value="summary" label="Summary" />
<Tab value="pools" label="Pools" />
<Tab value="pods" label="Pods" />
<Tab value="license" label="License" />
</Tabs>
</Grid>
<Grid item xs={12}>
{selectedTab === 0 && (
<React.Fragment>
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={8}>
<table width={"100%"}>
<tr>
<td colSpan={4}>
<h2>Details</h2>
<hr className={classes.hrClass} />
</td>
</tr>
<tr>
<td className={classes.titleCol}>Capacity:</td>
<td>{niceBytes(capacity.toString(10))}</td>
<td className={classes.titleCol}>MinIO:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setUpdateMinioVersion(true);
}}
>
{tenant ? tenant.image : ""}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Clusters:</td>
<td>{poolCount}</td>
<td className={classes.titleCol}>Console:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setUpdateMinioVersion(true);
}}
>
{tenant ? tenant.console_image : ""}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Instances:</td>
<td>{instances}</td>
<td className={classes.titleCol}>Volumes:</td>
<td>{volumes}</td>
</tr>
{tenant?.endpoints && (
<tr>
<td className={classes.titleCol}>Endpoint:</td>
<td>
<a
href={tenant?.endpoints.minio}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.minio}
</a>
</td>
<td className={classes.titleCol}>Console:</td>
<td>
<a
href={tenant?.endpoints.console}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.console}
</a>
</td>
</tr>
)}
<tr>
<td className={classes.titleCol}>State:</td>
<td colSpan={3}>{tenant?.currentState}</td>
</tr>
</table>
</Grid>
<Grid item xs={4}>
<UsageBarWrapper
currValue={usage}
maxValue={tenant ? tenant.total_size : 0}
label={"Storage"}
renderFunction={niceBytes}
error={usageError}
loading={loadingUsage}
/>
<h4>
{tenant && tenant.status && (
<span
className={healthStatusToClass(
tenant.status.health_status
)}
>
&nbsp;
</span>
)}
Health
</h4>
<table>
<tr>
<td className={classes.healthCol}>Drives Online</td>
<td>
{tenant?.status?.drives_online
? tenant?.status?.drives_online
: 0}
</td>
</tr>
<tr>
<td className={classes.healthCol}>Drives Offline</td>
<td>
{tenant?.status?.drives_offline
? tenant?.status?.drives_offline
: 0}
</td>
</tr>
<tr>
<td className={classes.healthCol}>Write Quorum</td>
<td>
{tenant?.status?.write_quorum
? tenant?.status?.write_quorum
: 0}
</td>
</tr>
</table>
</Grid>
</Grid>
</Paper>
<br />
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<table width={"100%"}>
<tr>
<td colSpan={4}>
<h2>Features</h2>
<hr className={classes.hrClass} />
</td>
</tr>
<tr>
<td className={classes.titleCol}>Logs:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{logEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
<td className={classes.titleCol}>Monitoring:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{monitoringEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Encryption:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
<td></td>
<td></td>
</tr>
<tr>
{adEnabled ||
(!adEnabled && !oicEnabled && (
<React.Fragment>
<td className={classes.titleCol}>
Active Directory:
</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{adEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</React.Fragment>
))}
{oicEnabled ||
(!oicEnabled && !adEnabled && (
<React.Fragment>
<td className={classes.titleCol}>OpenID:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{oicEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</React.Fragment>
))}
</tr>
</table>
</Grid>
</Grid>
</Paper>
</React.Fragment>
)}
{selectedTab === 1 && (
<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>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddPool(true);
}}
>
Expand Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={[
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={false}
records={pools}
entityName="Servers"
idField="name"
/>
</Grid>
</Grid>
)}
{selectedTab === 2 && (
<React.Fragment>
<br />
<TableWrapper
itemActions={podTableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Status", elementKey: "status" },
{ label: "Age", elementKey: "time" },
{ label: "Pod IP", elementKey: "podIP" },
{
label: "Restarts",
elementKey: "restarts",
renderFunction: (input) => {
return input != null ? input : 0;
},
},
{ label: "Node", elementKey: "node" },
]}
isLoading={false}
records={pods}
entityName="Servers"
idField="name"
<Router history={history}>
<Switch>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/summary"
component={TenantSummary}
/>
</React.Fragment>
)}
{selectedTab === 3 && (
<React.Fragment>
<br />
<Grid container>
<Grid item xs={12}>
<SubnetLicenseTenant
tenant={tenant}
loadingLicenseInfo={loadingLicenseInfo}
loadingActivateProduct={loadingActivateProduct}
licenseInfo={licenseInfo}
activateProduct={activateProduct}
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/pools"
component={PoolsSummary}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/pods"
component={PodsSummary}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/license"
component={TenantLicense}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName"
component={() => (
<Redirect
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}/summary`}
/>
</Grid>
</Grid>
</React.Fragment>
)}
)}
/>
</Switch>
</Router>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};
const connector = connect(null, {
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
currentTab: state.tenants.tenantDetails.currentTab,
selectedTenant: state.tenants.tenantDetails.currentTenant,
selectedNamespace: state.tenants.tenantDetails.currentNamespace,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
setTenantTab,
});
export default withStyles(styles)(connector(TenantDetails));

View File

@@ -0,0 +1,135 @@
// 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, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import api from "../../../../common/api";
import { ITenant } from "../ListTenants/types";
import { LicenseInfo } from "../../License/types";
import { setErrorSnackMessage } from "../../../../actions";
import SubnetLicenseTenant from "./SubnetLicenseTenant";
import { AppState } from "../../../../store";
import { setTenantDetailsLoad } from "../actions";
import { CircularProgress } from "@material-ui/core";
interface ITenantLicense {
classes: any;
loadingTenant: boolean;
tenant: ITenant | null;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
loaderAlign: {
textAlign: "center",
},
...containerForHeader(theme.spacing(4)),
});
const TenantLicense = ({
classes,
tenant,
loadingTenant,
setTenantDetailsLoad,
}: ITenantLicense) => {
const [licenseInfo, setLicenseInfo] = useState<LicenseInfo>();
const [loadingLicenseInfo, setLoadingLicenseInfo] = useState<boolean>(true);
const [loadingActivateProduct, setLoadingActivateProduct] =
useState<boolean>(false);
const activateProduct = (namespace: string, tenant: string) => {
if (loadingActivateProduct) {
return;
}
setLoadingActivateProduct(true);
api
.invoke(
"POST",
`/api/v1/subscription/namespaces/${namespace}/tenants/${tenant}/activate`,
{}
)
.then(() => {
setLoadingActivateProduct(false);
setTenantDetailsLoad(true);
setLoadingLicenseInfo(true);
})
.catch((err) => {
setLoadingActivateProduct(false);
setErrorSnackMessage(err);
});
};
useEffect(() => {
if (loadingLicenseInfo) {
api
.invoke("GET", `/api/v1/subscription/info`)
.then((res: LicenseInfo) => {
setLicenseInfo(res);
setLoadingLicenseInfo(false);
})
.catch((err: any) => {
setLoadingLicenseInfo(false);
});
}
}, [loadingLicenseInfo]);
return (
<Fragment>
<br />
{loadingTenant ? (
<div className={classes.loaderAlign}>
<CircularProgress />
</div>
) : (
<Fragment>
{tenant && (
<Grid container>
<Grid item xs={12}>
<SubnetLicenseTenant
tenant={tenant}
loadingLicenseInfo={loadingLicenseInfo}
loadingActivateProduct={loadingActivateProduct}
licenseInfo={licenseInfo}
activateProduct={activateProduct}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
tenant: state.tenants.tenantDetails.tenantInfo,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setTenantDetailsLoad,
});
export default withStyles(styles)(connector(TenantLicense));

View File

@@ -0,0 +1,446 @@
// 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, useEffect, useState } from "react";
import { connect } from "react-redux";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import { Button, CircularProgress } from "@material-ui/core";
import Paper from "@material-ui/core/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 { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
interface ITenantsSummary {
classes: any;
match: any;
tenant: ITenant | null;
logEnabled: boolean;
monitoringEnabled: boolean;
encryptionEnabled: boolean;
adEnabled: boolean;
oicEnabled: boolean;
loadingTenant: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
interface ITenantUsage {
used: string;
disk_used: string;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
redState: {
color: theme.palette.error.main,
},
yellowState: {
color: theme.palette.warning.main,
},
greenState: {
color: theme.palette.success.main,
},
greyState: {
color: "grey",
},
centerAlign: {
textAlign: "center",
},
...containerForHeader(theme.spacing(4)),
});
const TenantSummary = ({
classes,
match,
tenant,
logEnabled,
monitoringEnabled,
encryptionEnabled,
adEnabled,
oicEnabled,
loadingTenant,
setErrorSnackMessage,
}: ITenantsSummary) => {
const [capacity, setCapacity] = useState<number>(0);
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"];
const tenantNamespace = match.params["tenantNamespace"];
const healthStatusToClass = (health_status: string) => {
return health_status === "red"
? classes.redState
: health_status === "yellow"
? classes.yellowState
: health_status === "green"
? classes.greenState
: 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) => {
setUsageError(err);
setUsage(0);
setLoadingUsage(false);
});
}
}, [tenantName, tenantNamespace, loadingUsage]);
useEffect(() => {
if (tenant) {
const res = tenant;
const resPools = !res.pools ? [] : res.pools;
let totalInstances = 0;
let totalVolumes = 0;
let poolNamedIndex = 0;
for (let pool of resPools) {
const cap =
pool.volumes_per_server *
pool.servers *
pool.volume_configuration.size;
pool.label = `pool-${poolNamedIndex}`;
if (pool.name === undefined || pool.name === "") {
pool.name = pool.label;
}
pool.capacity = niceBytes(cap + "");
pool.volumes = pool.servers * pool.volumes_per_server;
totalInstances += pool.servers;
totalVolumes += pool.volumes;
poolNamedIndex += 1;
}
setCapacity(res.total_size || 0);
setPoolCount(resPools.length);
setVolumes(totalVolumes);
setInstances(totalInstances);
}
}, [tenant]);
return (
<Fragment>
{updateMinioVersion && (
<UpdateTenantModal
open={updateMinioVersion}
closeModalAndRefresh={() => {
setUpdateMinioVersion(false);
}}
idTenant={tenantName}
namespace={tenantNamespace}
/>
)}
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={8}>
<table width={"100%"}>
<tbody>
<tr>
<td colSpan={4}>
<h2>Details</h2>
<hr className={classes.hrClass} />
</td>
</tr>
{loadingTenant ? (
<tr>
<td colSpan={4} className={classes.centerAlign}>
<CircularProgress />
</td>
</tr>
) : (
<Fragment>
<tr>
<td className={classes.titleCol}>Capacity:</td>
<td>{niceBytes(capacity.toString(10))}</td>
<td className={classes.titleCol}>MinIO:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setUpdateMinioVersion(true);
}}
>
{tenant ? tenant.image : ""}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Clusters:</td>
<td>{poolCount}</td>
<td className={classes.titleCol}>Console:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setUpdateMinioVersion(true);
}}
>
{tenant ? tenant.console_image : ""}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Instances:</td>
<td>{instances}</td>
<td className={classes.titleCol}>Volumes:</td>
<td>{volumes}</td>
</tr>
{tenant?.endpoints && (
<tr>
<td className={classes.titleCol}>Endpoint:</td>
<td>
<a
href={tenant?.endpoints.minio}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.minio}
</a>
</td>
<td className={classes.titleCol}>Console:</td>
<td>
<a
href={tenant?.endpoints.console}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.console}
</a>
</td>
</tr>
)}
<tr>
<td className={classes.titleCol}>State:</td>
<td colSpan={3}>{tenant?.currentState}</td>
</tr>
</Fragment>
)}
</tbody>
</table>
</Grid>
<Grid item xs={4}>
{loadingTenant ? (
<div className={classes.centerAlign}><CircularProgress /></div>
) : (
<Fragment>
<UsageBarWrapper
currValue={usage}
maxValue={tenant ? tenant.total_size : 0}
label={"Storage"}
renderFunction={niceBytes}
error={usageError}
loading={loadingUsage}
/>
<h4>
{tenant && tenant.status && (
<span
className={healthStatusToClass(
tenant.status.health_status
)}
>
&nbsp;
</span>
)}
Health
</h4>
<table>
<tbody>
<tr>
<td className={classes.healthCol}>Drives Online</td>
<td>
{tenant?.status?.drives_online
? tenant?.status?.drives_online
: 0}
</td>
</tr>
<tr>
<td className={classes.healthCol}>Drives Offline</td>
<td>
{tenant?.status?.drives_offline
? tenant?.status?.drives_offline
: 0}
</td>
</tr>
<tr>
<td className={classes.healthCol}>Write Quorum</td>
<td>
{tenant?.status?.write_quorum
? tenant?.status?.write_quorum
: 0}
</td>
</tr>
</tbody>
</table>
</Fragment>
)}
</Grid>
</Grid>
</Paper>
<br />
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<table width={"100%"}>
<tbody>
<tr>
<td colSpan={4}>
<h2>Features</h2>
<hr className={classes.hrClass} />
</td>
</tr>
{loadingTenant ? (
<tr>
<td className={classes.centerAlign} colSpan={4}>
<CircularProgress />
</td>
</tr>
) : (
<Fragment>
<tr>
<td className={classes.titleCol}>Logs:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{logEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
<td className={classes.titleCol}>Monitoring:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{monitoringEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Encryption:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
<td></td>
<td></td>
</tr>
<tr>
{adEnabled ||
(!adEnabled && !oicEnabled && (
<React.Fragment>
<td className={classes.titleCol}>
Active Directory:
</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{adEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</React.Fragment>
))}
{oicEnabled ||
(!oicEnabled && !adEnabled && (
<React.Fragment>
<td className={classes.titleCol}>OpenID:</td>
<td>
<Button
color="primary"
className={classes.anchorButton}
>
{oicEnabled ? "Enabled" : "Disabled"}
</Button>
</td>
</React.Fragment>
))}
</tr>
</Fragment>
)}
</tbody>
</table>
</Grid>
</Grid>
</Paper>
</Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
selectedTenant: state.tenants.tenantDetails.currentTenant,
tenant: state.tenants.tenantDetails.tenantInfo,
logEnabled: get(state.tenants.tenantDetails.tenantInfo, "logEnabled", false),
monitoringEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"monitoringEnabled",
false
),
encryptionEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"encryptionEnabled",
false
),
adEnabled: get(state.tenants.tenantDetails.tenantInfo, "idpAdEnabled", false),
oicEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"idpOicEnabled",
false
),
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(TenantSummary));

View File

@@ -14,6 +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 { ITenant } from "./ListTenants/types";
import { Opts } from "./ListTenants/utils";
import {
ADD_TENANT_SET_ADVANCED_MODE,
@@ -35,6 +36,10 @@ import {
ADD_TENANT_ENCRYPTION_VAULT_CA,
ADD_TENANT_ENCRYPTION_GEMALTO_CA,
ADD_TENANT_RESET_FORM,
TENANT_DETAILS_SET_LOADING,
TENANT_DETAILS_SET_TENANT,
TENANT_DETAILS_SET_CURRENT_TENANT,
TENANT_DETAILS_SET_TAB,
} from "./types";
// Basic actions
@@ -220,3 +225,32 @@ export const resetAddTenantForm = () => {
type: ADD_TENANT_RESET_FORM,
};
};
export const setTenantDetailsLoad = (loading: boolean) => {
return {
type: TENANT_DETAILS_SET_LOADING,
state: loading,
}
};
export const setTenantName = (tenantName: string, tenantNamespace: string) => {
return {
type: TENANT_DETAILS_SET_CURRENT_TENANT,
name: tenantName,
namespace: tenantNamespace,
}
};
export const setTenantInfo = (tenant: ITenant | null) => {
return {
type: TENANT_DETAILS_SET_TENANT,
tenant
}
};
export const setTenantTab = (tab: string) => {
return {
type: TENANT_DETAILS_SET_TAB,
tab,
}
};

View File

@@ -36,6 +36,10 @@ import {
ADD_TENANT_ENCRYPTION_VAULT_CA,
ADD_TENANT_ENCRYPTION_GEMALTO_CA,
ADD_TENANT_RESET_FORM,
TENANT_DETAILS_SET_LOADING,
TENANT_DETAILS_SET_CURRENT_TENANT,
TENANT_DETAILS_SET_TENANT,
TENANT_DETAILS_SET_TAB,
} from "./types";
import { KeyPair } from "./ListTenants/utils";
import { getRandomString } from "./utils";
@@ -227,6 +231,13 @@ const initialState: ITenantState = {
},
},
},
tenantDetails: {
currentTenant: "",
currentNamespace: "",
loadingTenant: false,
tenantInfo: null,
currentTab: "summary",
},
};
export function tenantsReducer(
@@ -624,6 +635,49 @@ export function tenantsReducer(
},
},
};
case TENANT_DETAILS_SET_LOADING:
const tenantDetails = {
...state.tenantDetails,
loadingTenant: action.state,
};
return {
...state,
tenantDetails: {
...tenantDetails,
},
};
case TENANT_DETAILS_SET_CURRENT_TENANT:
const currentTenant = {
...state.tenantDetails,
currentTenant: action.name,
currentNamespace: action.namespace,
};
return {
...state,
tenantDetails: {
...currentTenant,
},
};
case TENANT_DETAILS_SET_TENANT:
let tenantData = null;
if (action.tenant) {
tenantData = { tenantInfo: { ...action.tenant } };
}
const setTenant = { ...state.tenantDetails, ...tenantData };
return {
...state,
tenantDetails: {
...setTenant,
},
};
case TENANT_DETAILS_SET_TAB:
const newTab = { ...state.tenantDetails, currentTab: action.tab };
return {
...state,
tenantDetails: {
...newTab,
},
};
default:
return state;
}

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { IErasureCodeCalc } from "../../../common/types";
import { IMemorySize } from "./ListTenants/types";
import { IMemorySize, ITenant } from "./ListTenants/types";
import { KeyPair, Opts } from "./ListTenants/utils";
export const ADD_TENANT_SET_CURRENT_PAGE = "ADD_TENANT/SET_CURRENT_PAGE";
@@ -52,6 +52,13 @@ export const ADD_TENANT_ENCRYPTION_VAULT_CA = "ADD_TENANT/ENCRYPTION_VAULT_CA";
export const ADD_TENANT_ENCRYPTION_GEMALTO_CA =
"ADD_TENANT/ENCRYPTION_GEMALTO_CA";
// Tenant Details
export const TENANT_DETAILS_SET_LOADING = "TENANT_DETAILS/SET_LOADING";
export const TENANT_DETAILS_SET_CURRENT_TENANT =
"TENANT_DETAILS/SET_CURRENT_TENANT";
export const TENANT_DETAILS_SET_TENANT = "TENANT_DETAILS/SET_TENANT";
export const TENANT_DETAILS_SET_TAB = "TENANT_DETAILS/SET_TAB";
export interface ICreateTenant {
page: number;
validPages: string[];
@@ -188,8 +195,17 @@ export interface ITenantAffinity {
withPodAntiAffinity: boolean;
}
export interface ITenantDetails {
currentTenant: string;
currentNamespace: string;
loadingTenant: boolean;
tenantInfo: ITenant | null;
currentTab: string;
}
export interface ITenantState {
createTenant: ICreateTenant;
tenantDetails: ITenantDetails;
}
export interface ILabelKeyPair {
@@ -308,6 +324,27 @@ interface ResetForm {
type: typeof ADD_TENANT_RESET_FORM;
}
interface SetLoadingTenant {
type: typeof TENANT_DETAILS_SET_LOADING;
state: boolean;
}
interface SetTenantName {
type: typeof TENANT_DETAILS_SET_CURRENT_TENANT;
name: string;
namespace: string;
}
interface SetTenantDetails {
type: typeof TENANT_DETAILS_SET_TENANT;
tenant: ITenant | null;
}
interface SetTenantTab {
type: typeof TENANT_DETAILS_SET_TAB;
tab: string;
}
export type FieldsToHandle = INameTenantFields;
export type TenantsManagementTypes =
@@ -329,4 +366,8 @@ export type TenantsManagementTypes =
| AddFileVaultCert
| AddFileVaultCa
| AddFileGemaltoCa
| ResetForm;
| ResetForm
| SetLoadingTenant
| SetTenantName
| SetTenantDetails
| SetTenantTab;

View File

@@ -91,7 +91,6 @@ const ListUsers = ({ classes, setErrorSnackMessage }: IUsersProps) => {
const [filter, setFilter] = useState<string>("");
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const [resetPWOpen, setResetPWOpen] = useState<boolean>(false);
const [ChangeUserPasswordModalOpen, setChangeUserPasswordModalOpen] =
useState<boolean>(false);

View File

@@ -66,8 +66,6 @@ const UserServiceAccountsPanel = ({
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [newServiceAccount, setNewServiceAccount] =
useState<NewServiceAccount | null>(null);
const [changePasswordModalOpen, setChangePasswordModalOpen] =
useState<boolean>(false);
useEffect(() => {
fetchRecords();
@@ -88,7 +86,7 @@ const UserServiceAccountsPanel = ({
setLoading(false);
});
}
}, [loading, setLoading, setRecords, setErrorSnackMessage]);
}, [loading, setLoading, setRecords, setErrorSnackMessage, user]);
const fetchRecords = () => {
setLoading(true);