From 6c5f6934e7a76e6a5c5940aaf809f56fa76cde1f Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:18:16 -0700 Subject: [PATCH] Tenant Details Thunk (#2072) Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- .../Tenants/ListTenants/TenantListItem.tsx | 11 +++ .../Tenants/TenantDetails/TenantDetails.tsx | 92 +++++-------------- .../screens/Console/Tenants/tenantsSlice.ts | 14 +++ .../Tenants/thunks/tenantDetailsAsync.ts | 70 ++++++++++++++ 4 files changed, 117 insertions(+), 70 deletions(-) create mode 100644 portal-ui/src/screens/Console/Tenants/thunks/tenantDetailsAsync.ts diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/TenantListItem.tsx b/portal-ui/src/screens/Console/Tenants/ListTenants/TenantListItem.tsx index c1f553238..22d85cbf7 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/TenantListItem.tsx +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/TenantListItem.tsx @@ -26,6 +26,9 @@ import { niceBytes, niceBytesInt } from "../../../../common/utils"; import InformationItem from "./InformationItem"; import TenantCapacity from "./TenantCapacity"; import { DrivesIcon } from "../../../../icons"; +import { setTenantName } from "../tenantsSlice"; +import { getTenantAsync } from "../thunks/tenantDetailsAsync"; +import { useDispatch } from "react-redux"; const styles = (theme: Theme) => createStyles({ @@ -104,6 +107,7 @@ interface ITenantListItem { } const TenantListItem = ({ tenant, classes }: ITenantListItem) => { + const dispatch = useDispatch(); const healthStatusToClass = (health_status: string) => { switch (health_status) { case "red": @@ -174,6 +178,13 @@ const TenantListItem = ({ tenant, classes }: ITenantListItem) => { } const openTenantDetails = () => { + dispatch( + setTenantName({ + name: tenant.name, + namespace: tenant.namespace, + }) + ); + dispatch(getTenantAsync()); history.push(`/namespaces/${tenant.namespace}/tenants/${tenant.name}`); }; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx index 5d28dd63e..d94fc1298 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx @@ -22,16 +22,12 @@ import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; import get from "lodash/get"; import Grid from "@mui/material/Grid"; - -import { ITenant } from "../ListTenants/types"; import { containerForHeader, pageContentStyles, tenantDetailsStyles, } from "../../Common/FormComponents/common/styleLibrary"; import { AppState } from "../../../../store"; -import { ErrorResponseHandler } from "../../../../common/types"; -import api from "../../../../common/api"; import PageHeader from "../../Common/PageHeader/PageHeader"; import { CircleIcon, MinIOTierIconXs, TrashIcon } from "../../../../icons"; import { niceBytes } from "../../../../common/utils"; @@ -46,15 +42,10 @@ import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton"; import withSuspense from "../../Common/Components/withSuspense"; import { IAM_PAGES } from "../../../../common/SecureComponent/permissions"; import { tenantIsOnline } from "../ListTenants/utils"; -import { - setErrorSnackMessage, - setSnackBarMessage, -} from "../../../../systemSlice"; -import { - setTenantDetailsLoad, - setTenantInfo, - setTenantName, -} from "../tenantsSlice"; +import { setSnackBarMessage } from "../../../../systemSlice"; +import { setTenantDetailsLoad, setTenantName } from "../tenantsSlice"; +import { getTenantAsync } from "../thunks/tenantDetailsAsync"; +import { LinearProgress } from "@mui/material"; const TenantYAML = withSuspense(React.lazy(() => import("./TenantYAML"))); const TenantSummary = withSuspense(React.lazy(() => import("./TenantSummary"))); @@ -188,23 +179,21 @@ const TenantDetails = ({ classes, match, history }: ITenantDetailsProps) => { const tenantNamespace = match.params["tenantNamespace"]; const [deleteOpen, setDeleteOpen] = useState(false); + // if the current tenant selected is not the one in the redux, reload it useEffect(() => { - if (!loadingTenant) { - if ( - tenantName !== selectedTenant || - tenantNamespace !== selectedNamespace - ) { - dispatch( - setTenantName({ - name: tenantName, - namespace: tenantNamespace, - }) - ); - dispatch(setTenantDetailsLoad(true)); - } + if ( + selectedNamespace !== tenantNamespace || + selectedTenant !== tenantName + ) { + dispatch( + setTenantName({ + name: tenantName, + namespace: tenantNamespace, + }) + ); + dispatch(getTenantAsync()); } }, [ - loadingTenant, selectedTenant, selectedNamespace, dispatch, @@ -212,48 +201,6 @@ const TenantDetails = ({ classes, match, history }: ITenantDetailsProps) => { tenantNamespace, ]); - useEffect(() => { - if (loadingTenant) { - api - .invoke( - "GET", - `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}` - ) - .then((res: ITenant) => { - // add computed fields - 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; - } - res.total_instances = totalInstances; - res.total_volumes = totalVolumes; - - dispatch(setTenantInfo(res)); - dispatch(setTenantDetailsLoad(false)); - }) - .catch((err: ErrorResponseHandler) => { - dispatch(setErrorSnackMessage(err)); - dispatch(setTenantDetailsLoad(false)); - }); - } - }, [loadingTenant, tenantNamespace, tenantName, dispatch]); - const path = get(match, "path", "/"); const splitSections = path.split("/"); @@ -332,6 +279,11 @@ const TenantDetails = ({ classes, match, history }: ITenantDetailsProps) => { /> + {loadingTenant && ( + + + + )} { variant="outlined" aria-label="Refresh List" onClick={() => { - dispatch(setTenantDetailsLoad(true)); + dispatch(getTenantAsync()); }} > Refresh diff --git a/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts b/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts index 2aeaa1c12..ec171288c 100644 --- a/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts +++ b/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts @@ -25,6 +25,7 @@ import get from "lodash/get"; import { has } from "lodash"; import { Opts } from "./ListTenants/utils"; import { ITenant } from "./ListTenants/types"; +import { getTenantAsync } from "./thunks/tenantDetailsAsync"; export interface FileValue { fileName: string; @@ -247,6 +248,19 @@ export const tenantSlice = createSlice({ state.tenantDetails.poolDetailsOpen = action.payload; }, }, + extraReducers: (builder) => { + builder + .addCase(getTenantAsync.pending, (state) => { + state.tenantDetails.loadingTenant = true; + }) + .addCase(getTenantAsync.rejected, (state) => { + state.tenantDetails.loadingTenant = false; + }) + .addCase(getTenantAsync.fulfilled, (state, action) => { + state.tenantDetails.loadingTenant = false; + state.tenantDetails.tenantInfo = action.payload; + }); + }, }); // Action creators are generated for each case reducer function diff --git a/portal-ui/src/screens/Console/Tenants/thunks/tenantDetailsAsync.ts b/portal-ui/src/screens/Console/Tenants/thunks/tenantDetailsAsync.ts new file mode 100644 index 000000000..0982b50a8 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/thunks/tenantDetailsAsync.ts @@ -0,0 +1,70 @@ +// 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 . + +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { AppState } from "../../../../store"; +import { niceBytes } from "../../../../common/utils"; +import { ITenant } from "../ListTenants/types"; +import api from "../../../../common/api"; +import { ErrorResponseHandler } from "../../../../common/types"; +import { setErrorSnackMessage } from "../../../../systemSlice"; + +export const getTenantAsync = createAsyncThunk( + "tenantDetails/getTenantAsync", + async (_, { getState, rejectWithValue, dispatch }) => { + const state = getState() as AppState; + + const currentNamespace = state.tenants.tenantDetails.currentNamespace; + const currentTenant = state.tenants.tenantDetails.currentTenant; + + return api + .invoke( + "GET", + `/api/v1/namespaces/${currentNamespace}/tenants/${currentTenant}` + ) + .then((res: ITenant) => { + // add computed fields + 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; + } + res.total_instances = totalInstances; + res.total_volumes = totalVolumes; + + return res; + }) + .catch((err: ErrorResponseHandler) => { + dispatch(setErrorSnackMessage(err)); + return rejectWithValue(err); + }); + } +);