Added DirectPV mode to Operator console (#2203)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-08-01 12:57:31 -05:00
committed by GitHub
parent 1deb6371ed
commit ad4b9c050a
32 changed files with 1332 additions and 49 deletions

View File

@@ -7137,8 +7137,8 @@ SOFTWARE.
================================================================
github.com/minio/direct-csi
https://github.com/minio/direct-csi
github.com/minio/directpv
https://github.com/minio/directpv
----------------------------------------------------------------
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

View File

@@ -37,6 +37,9 @@ import (
// swagger:model loginDetails
type LoginDetails struct {
// is direct p v
IsDirectPV bool `json:"isDirectPV,omitempty"`
// login strategy
// Enum: [form redirect service-account redirect-service-account]
LoginStrategy string `json:"loginStrategy,omitempty"`

View File

@@ -37,6 +37,9 @@ import (
// swagger:model operatorSessionResponse
type OperatorSessionResponse struct {
// direct p v
DirectPV bool `json:"directPV,omitempty"`
// features
Features []string `json:"features"`

View File

@@ -69,3 +69,10 @@ func getK8sSAToken() string {
func getMarketplace() string {
return env.Get(ConsoleMarketplace, "")
}
// Get DirectPVMode
func getDirectPVEnabled() bool {
currentMode := env.Get(DirectPVMode, "off")
return currentMode == "on"
}

View File

@@ -25,6 +25,9 @@ const (
prometheusPath = "prometheus.io/path"
prometheusPort = "prometheus.io/port"
prometheusScrape = "prometheus.io/scrape"
// Constants for DirectPV
DirectPVMode = "DIRECTPV_MODE"
)
// Image versions

View File

@@ -3415,6 +3415,9 @@ func init() {
"loginDetails": {
"type": "object",
"properties": {
"isDirectPV": {
"type": "boolean"
},
"loginStrategy": {
"type": "string",
"enum": [
@@ -3648,6 +3651,9 @@ func init() {
"operatorSessionResponse": {
"type": "object",
"properties": {
"directPV": {
"type": "boolean"
},
"features": {
"type": "array",
"items": {
@@ -9174,6 +9180,9 @@ func init() {
"loginDetails": {
"type": "object",
"properties": {
"isDirectPV": {
"type": "boolean"
},
"loginStrategy": {
"type": "string",
"enum": [
@@ -9363,6 +9372,9 @@ func init() {
"operatorSessionResponse": {
"type": "object",
"properties": {
"directPV": {
"type": "boolean"
},
"features": {
"type": "array",
"items": {

View File

@@ -118,6 +118,7 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDet
loginDetails := &models.LoginDetails{
LoginStrategy: loginStrategy,
Redirect: redirectURL,
IsDirectPV: getDirectPVEnabled(),
}
return loginDetails, nil
}

View File

@@ -52,6 +52,7 @@ func getSessionResponse(session *models.Principal, params authApi.SessionCheckPa
Operator: true,
Permissions: map[string][]string{},
Features: getListOfOperatorFeatures(),
DirectPV: getDirectPVEnabled(),
}
return sessionResp, nil
}

View File

@@ -23,6 +23,7 @@ import { ErrorResponseHandler } from "./common/types";
import { ReplicationSite } from "./screens/Console/Configurations/SiteReplication/SiteReplication";
import { useSelector } from "react-redux";
import {
directPVMode,
globalSetDistributedSetup,
operatorMode,
selOpMode,
@@ -63,6 +64,7 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
// check for tenants presence, that indicates we are in operator mode
if (res.operator) {
dispatch(operatorMode(true));
dispatch(directPVMode(!!res.directPV));
document.title = "MinIO Operator";
}
})

View File

@@ -212,6 +212,11 @@ export const IAM_PAGES = {
"/namespaces/:tenantNamespace/tenants/:tenantName/events",
NAMESPACE_TENANT_CSR: "/namespaces/:tenantNamespace/tenants/:tenantName/csr",
OPERATOR_MARKETPLACE: "/marketplace",
/* DirectPV */
DIRECTPV_STORAGE: "/storage",
DIRECTPV_DRIVES: "/drives",
DIRECTPV_VOLUMES: "/volumes",
};
// roles

View File

@@ -0,0 +1,56 @@
// 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, { SVGProps } from "react";
const DirectPVLogo = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
{...props}
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 416.5 121.19"
>
<rect id="Rectangle_2" x="60.81" y=".48" width="10.5" height="30.9" />
<path
id="Path_1"
d="M48.11,.98L26.81,13.98c-.3,.2-.7,.2-1,0L4.51,.98c-.5-.3-1-.4-1.5-.4h0C1.41,.58,.11,1.88,.11,3.48V31.48H10.61v-13.4c0-.6,.5-1,1.1-1,.2,0,.4,.1,.5,.2l11.9,7.3c1.2,.7,2.6,.7,3.8,0l12.6-7.4c.5-.3,1.1-.1,1.4,.4,.1,.2,.1,.3,.1,.5v13.4h10.5V3.48c0-1.6-1.3-2.9-2.9-2.9h0c-.5-.1-1,.1-1.5,.4Z"
/>
<path
id="Path_2"
d="M123.61,.48h-10.6V14.58c0,.6-.5,1-1,1-.2,0-.3,0-.5-.1L83.91,.88c-.4-.2-.9-.3-1.4-.3h0c-1.6,0-2.9,1.3-2.9,2.9V31.48h10.5v-14.1c0-.6,.5-1,1.1-1,.2,0,.3,0,.5,.1l27.6,14.7c.4,.2,.9,.3,1.4,.3h0c1.6,0,2.9-1.3,2.9-2.9V.48Z"
/>
<path id="Path_3" d="M131.81,31.5V.5h4.8V31.4l-4.8,.1Z" />
<path
id="Path_4"
d="M165.01,32c-13,0-22.2-6.2-22.2-16S152.11,0,165.01,0s22.2,6.2,22.2,16-9.1,16-22.2,16Zm0-27.9c-9.6,0-17.1,4.2-17.1,11.9s7.4,11.9,17.1,11.9,17.1-4.2,17.1-11.9-7.5-11.9-17.1-11.9Z"
/>
<path d="M0,50.99H26c11.3,0,20.3,3.1,26.9,9.4,6.6,6.3,10,14.6,10,25.2s-3.3,18.9-10,25.2c-6.6,6.3-15.6,9.4-26.9,9.4H0V50.99Zm26,7.5H7.9v54.2H26c9.2,0,16.2-2.4,21.1-7.3s7.4-11.5,7.4-19.8-2.5-14.9-7.4-19.8c-4.8-4.8-11.9-7.3-21.1-7.3Z" />
<path d="M81.3,51.59c1.4,0,2.7,.5,3.7,1.5s1.5,2.2,1.5,3.7-.5,2.7-1.5,3.7-2.2,1.5-3.7,1.5-2.7-.5-3.7-1.5-1.5-2.2-1.5-3.7,.5-2.7,1.5-3.7c1-1,2.2-1.5,3.7-1.5Zm3.7,21.8v46.8h-7.4v-46.8h7.4Z" />
<path d="M123.4,72.49c3.4,0,6.3,.5,8.6,1.6l-1.8,7.3c-2.3-1.3-5-1.9-8.1-1.9-3.7,0-6.7,1.3-9,4s-3.5,6.2-3.5,10.6v26.2h-7.4v-46.9h7.3v6.6c1.6-2.4,3.6-4.3,6-5.6s5.1-1.9,7.9-1.9Z" />
<path d="M160.9,72.49c6.4,0,11.6,2.3,15.7,6.8,4.1,4.5,6.2,10.3,6.2,17.4,0,1,0,1.9-.1,2.8h-37.5c.5,4.9,2.3,8.6,5.3,11.3,3.1,2.7,6.8,4,11.2,4,5.6,0,10.5-2,14.8-5.9l4,5c-5.1,4.9-11.5,7.3-19.2,7.3-6.9,0-12.6-2.2-17-6.7s-6.6-10.3-6.6-17.6,2.2-12.9,6.6-17.5c4.4-4.7,9.9-6.9,16.6-6.9Zm-.1,6.5c-4.2,0-7.7,1.3-10.4,4-2.7,2.6-4.4,6.1-5,10.4h30c-.5-4.4-2-7.9-4.7-10.5-2.6-2.6-6-3.9-9.9-3.9Z" />
<path d="M217.2,72.49c3.5,0,6.7,.6,9.7,1.9s5.5,3.1,7.6,5.5l-4.7,5c-3.7-3.6-8-5.4-12.8-5.4s-8.6,1.6-11.8,4.9c-3.1,3.3-4.7,7.4-4.7,12.4s1.6,9.2,4.7,12.5c3.1,3.3,7.1,4.9,11.8,4.9s9.2-1.8,12.9-5.5l4.6,5c-2.1,2.4-4.7,4.2-7.7,5.5s-6.2,1.9-9.7,1.9c-7.1,0-12.9-2.3-17.4-6.9s-6.8-10.4-6.8-17.4,2.3-12.8,6.8-17.4c4.6-4.6,10.4-6.9,17.5-6.9Z" />
<path d="M279.6,73.39v6.7h-20.2v23.6c0,3.5,.8,6.2,2.5,7.9,1.7,1.7,3.9,2.6,6.8,2.6,3.5,0,6.8-1.1,9.8-3.3l3.5,5.6c-4.1,3.1-8.7,4.6-13.9,4.6s-9.1-1.4-11.9-4.3-4.2-7.2-4.2-13v-23.7h-10.2v-6.7h10.2v-14.9h7.4v14.9h20.2Z" />
<path d="M320,92.39h-18.2v27.8h-8V50.99h26.1c7.7,0,13.7,1.8,18,5.5s6.4,8.7,6.4,15.2-2.1,11.5-6.4,15.2c-4.2,3.6-10.2,5.5-17.9,5.5Zm-.2-33.9h-17.9v26.4h17.9c5.4,0,9.4-1.1,12.2-3.3,2.8-2.2,4.2-5.5,4.2-9.9s-1.4-7.7-4.2-9.9-6.9-3.3-12.2-3.3Z" />
<path d="M416.5,50.99l-27.7,69.1h-9.8l-27.7-69.1h8.4l24.3,61.2,24.3-61.2h8.2Z" />
</svg>
);
};
export default DirectPVLogo;

View File

@@ -39,7 +39,7 @@ import { useSelector } from "react-redux";
import useApi from "./Common/Hooks/useApi";
import { Bucket, BucketList } from "./Buckets/types";
import { selFeatures } from "./consoleSlice";
import { selOpMode } from "../../systemSlice";
import { selOpMode, selDirectPVMode } from "../../systemSlice";
const useStyles = makeStyles((theme: Theme) => ({
resultItem: {
@@ -128,6 +128,7 @@ const KBarStateChangeMonitor = ({
const CommandBar = () => {
const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode);
const features = useSelector(selFeatures);
const navigate = useNavigate();
@@ -148,6 +149,7 @@ const CommandBar = () => {
const initialActions: Action[] = routesAsKbarActions(
features,
operatorMode,
directPVMode,
buckets,
navigate
);

View File

@@ -24,12 +24,13 @@ import IconButton from "@mui/material/IconButton";
import { AppState, useAppDispatch } from "../../../../store";
import OperatorLogo from "../../../../icons/OperatorLogo";
import ConsoleLogo from "../../../../icons/ConsoleLogo";
import DirectPVLogo from "../../../../icons/DirectPVLogo";
import { CircleIcon, ObjectManagerIcon } from "../../../../icons";
import { Box } from "@mui/material";
import { toggleList } from "../../ObjectBrowser/objectBrowserSlice";
import { selFeatures } from "../../consoleSlice";
import { selOpMode } from "../../../../systemSlice";
import { selDirectPVMode, selOpMode } from "../../../../systemSlice";
const styles = (theme: Theme) =>
createStyles({
@@ -107,6 +108,7 @@ const PageHeader = ({
(state: AppState) => state.system.sidebarOpen
);
const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode);
const managerObjects = useSelector(
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
);
@@ -151,7 +153,13 @@ const PageHeader = ({
>
{!sidebarOpen && (
<div className={classes.logo}>
{operatorMode ? <OperatorLogo /> : <ConsoleLogo />}
{!operatorMode && !directPVMode ? (
<ConsoleLogo />
) : (
<Fragment>
{directPVMode ? <DirectPVLogo /> : <OperatorLogo />}
</Fragment>
)}
</div>
)}
<Box

View File

@@ -52,6 +52,7 @@ import EditPool from "./Tenants/TenantDetails/Pools/EditPool/EditPool";
import ComponentsScreen from "./Common/ComponentsScreen";
import {
menuOpen,
selDirectPVMode,
selDistSet,
selOpMode,
serverIsLoading,
@@ -145,6 +146,12 @@ const AddReplicationSites = React.lazy(
() => import("./Configurations/SiteReplication/AddReplicationSites")
);
const StoragePVCs = React.lazy(() => import("./Storage/StoragePVCs"));
const DirectPVDrives = React.lazy(() => import("./DirectPV/DirectPVDrives"));
const DirectPVVolumes = React.lazy(() => import("./DirectPV/DirectPVVolumes"));
const styles = (theme: Theme) =>
createStyles({
root: {
@@ -187,6 +194,7 @@ const Console = ({ classes }: IConsoleProps) => {
const features = useSelector(selFeatures);
const distributedSetup = useSelector(selDistSet);
const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode);
const snackBarMessage = useSelector(
(state: AppState) => state.system.snackBar
);
@@ -461,9 +469,38 @@ const Console = ({ classes }: IConsoleProps) => {
},
];
const allowedRoutes = (
operatorMode ? operatorConsoleRoutes : consoleAdminRoutes
).filter((route: any) =>
const directPVRoutes: IRouteRule[] = [
{
component: StoragePVCs,
path: IAM_PAGES.DIRECTPV_STORAGE,
forceDisplay: true,
},
{
component: DirectPVDrives,
path: IAM_PAGES.DIRECTPV_DRIVES,
forceDisplay: true,
},
{
component: DirectPVVolumes,
path: IAM_PAGES.DIRECTPV_VOLUMES,
forceDisplay: true,
},
{
component: License,
path: IAM_PAGES.LICENSE,
forceDisplay: true,
},
];
let routes = consoleAdminRoutes;
if (directPVMode) {
routes = directPVRoutes;
} else if (operatorMode) {
routes = operatorConsoleRoutes;
}
const allowedRoutes = routes.filter((route: any) =>
obOnly
? route.path.includes("buckets")
: (route.forceDisplay ||

View File

@@ -0,0 +1,347 @@
// 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, InputAdornment, TextField } from "@mui/material";
import get from "lodash/get";
import GroupIcon from "@mui/icons-material/Group";
import { AddIcon, StorageIcon } from "../../../icons";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import {
IDirectPVDrives,
IDirectPVFormatResItem,
IDrivesResponse,
} from "./types";
import { niceBytes } from "../../../common/utils";
import { ErrorResponseHandler } from "../../../common/types";
import api from "../../../common/api";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import RefreshIcon from "../../../icons/RefreshIcon";
import SearchIcon from "../../../icons/SearchIcon";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
import HelpBox from "../../../common/HelpBox";
import BoxButton from "../Common/BoxButton/BoxButton";
import withSuspense from "../Common/Components/withSuspense";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
const FormatDrives = withSuspense(React.lazy(() => import("./FormatDrives")));
const FormatErrorsResult = withSuspense(
React.lazy(() => import("./FormatErrorsResult"))
);
interface IDirectPVMain {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
tableWrapper: {
height: "calc(100vh - 275px)",
},
linkItem: {
display: "default",
color: theme.palette.info.main,
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
color: "#000",
},
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
const DirectPVMain = ({ classes }: IDirectPVMain) => {
const [records, setRecords] = useState<IDirectPVDrives[]>([]);
const [filter, setFilter] = useState("");
const [checkedDrives, setCheckedDrives] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [formatOpen, setFormatOpen] = useState<boolean>(false);
const [formatAll, setFormatAll] = useState<boolean>(false);
const [formatErrorsResult, setFormatErrorsResult] = useState<
IDirectPVFormatResItem[]
>([]);
const [formatErrorsOpen, setFormatErrorsOpen] = useState<boolean>(false);
const [drivesToFormat, setDrivesToFormat] = useState<string[]>([]);
const [notAvailable, setNotAvailable] = useState<boolean>(true);
useEffect(() => {
if (loading) {
api
.invoke("GET", "/api/v1/directpv/drives")
.then((res: IDrivesResponse) => {
let drives: IDirectPVDrives[] = get(res, "drives", []);
if (!drives) {
drives = [];
}
drives = drives.map((item) => {
const newItem = { ...item };
newItem.joinName = `${newItem.node}:${newItem.drive}`;
return newItem;
});
drives.sort((d1, d2) => {
if (d1.drive > d2.drive) {
return 1;
}
if (d1.drive < d2.drive) {
return -1;
}
return 0;
});
setRecords(drives);
setLoading(false);
setNotAvailable(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setNotAvailable(true);
});
}
}, [loading, notAvailable]);
const formatAllDrives = () => {
const allDrives = records.map((item) => {
return `${item.node}:${item.drive}`;
});
setFormatAll(true);
setDrivesToFormat(allDrives);
setFormatOpen(true);
};
const formatSingleUnit = (driveID: string) => {
const selectedUnit = [driveID];
setDrivesToFormat(selectedUnit);
setFormatAll(false);
setFormatOpen(true);
};
const formatSelectedDrives = () => {
if (checkedDrives.length > 0) {
setDrivesToFormat(checkedDrives);
setFormatAll(false);
setFormatOpen(true);
}
};
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...checkedDrives]; // We clone the checkedDrives array
if (checked) {
// If the user has checked this field we need to push this to checkedDrivesList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setCheckedDrives(elements);
return elements;
};
const closeFormatModal = (
refresh: boolean,
errorsList: IDirectPVFormatResItem[]
) => {
setFormatOpen(false);
if (refresh) {
// Errors are present, we trigger the modal box to show these changes.
if (errorsList && errorsList.length > 0) {
setFormatErrorsResult(errorsList);
setFormatErrorsOpen(true);
}
setLoading(true);
setCheckedDrives([]);
}
};
const tableActions = [
{
type: "format",
onClick: formatSingleUnit,
sendOnlyId: true,
},
];
const filteredRecords: IDirectPVDrives[] = records.filter((elementItem) =>
elementItem.drive.includes(filter)
);
return (
<Fragment>
{formatOpen && (
<FormatDrives
closeFormatModalAndRefresh={closeFormatModal}
deleteOpen={formatOpen}
allDrives={formatAll}
drivesToFormat={drivesToFormat}
/>
)}
{formatErrorsOpen && (
<FormatErrorsResult
errorsList={formatErrorsResult}
open={formatErrorsOpen}
onCloseFormatErrorsList={() => {
setFormatErrorsOpen(false);
}}
/>
)}
<PageHeader label="Local Drives" />
<PageLayout>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Drives"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
disabled={notAvailable}
variant="standard"
/>
<BoxIconButton
color="primary"
aria-label="Refresh Tenant List"
onClick={() => {
setLoading(true);
}}
disabled={notAvailable}
size="large"
>
<RefreshIcon />
</BoxIconButton>
<BoxButton
variant="contained"
color="primary"
disabled={checkedDrives.length <= 0 || notAvailable}
onClick={formatSelectedDrives}
label={"Format Selected Drives"}
>
<GroupIcon />
</BoxButton>
<BoxButton
variant="contained"
color="primary"
label={"Format All Drives"}
onClick={formatAllDrives}
disabled={notAvailable}
>
<AddIcon />
</BoxButton>
</Grid>
<Grid item xs={12}>
{notAvailable && !loading ? (
<HelpBox
title={"Leverage locally attached drives"}
iconComponent={<StorageIcon />}
help={
<Fragment>
We can automatically provision persistent volumes (PVs) on top
locally attached drives on your Kubernetes nodes by leveraging
DirectPV.
<br />
<br />
For more information{" "}
<a
href="https://github.com/minio/directpv"
rel="noreferrer"
target="_blank"
className={classes.linkItem}
>
Visit DirectPV Documentation
</a>
</Fragment>
}
/>
) : (
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Drive",
elementKey: "drive",
},
{
label: "Capacity",
elementKey: "capacity",
renderFunction: niceBytes,
},
{
label: "Allocated",
elementKey: "allocated",
renderFunction: niceBytes,
},
{
label: "Volumes",
elementKey: "volumes",
},
{
label: "Node",
elementKey: "node",
},
{
label: "Status",
elementKey: "status",
},
]}
onSelect={selectionChanged}
selectedItems={checkedDrives}
isLoading={loading}
records={filteredRecords}
customPaperHeight={classes.tableWrapper}
entityName="Drives"
idField="joinName"
/>
)}
</Grid>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(DirectPVMain);

View File

@@ -0,0 +1,163 @@
// 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 get from "lodash/get";
import { useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, InputAdornment, TextField } from "@mui/material";
import { AppState, useAppDispatch } from "../../../store";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { IDirectPVVolumes, IVolumesResponse } from "./types";
import { niceBytes } from "../../../common/utils";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage } from "../../../systemSlice";
import api from "../../../common/api";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SearchIcon from "../../../icons/SearchIcon";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
interface IDirectPVVolumesProps {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
tableWrapper: {
height: "calc(100vh - 267px)",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
const DirectPVVolumes = ({ classes }: IDirectPVVolumesProps) => {
const dispatch = useAppDispatch();
const selectedDrive = useSelector(
(state: AppState) => state.directPV.selectedDrive
);
const [records, setRecords] = useState<IDirectPVVolumes[]>([]);
const [filter, setFilter] = useState("");
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/directpv/volumes?drives=${selectedDrive}`)
.then((res: IVolumesResponse) => {
let volumes = get(res, "volumes", []);
if (!volumes) {
volumes = [];
}
volumes.sort((d1, d2) => {
if (d1.volume > d2.volume) {
return 1;
}
if (d1.volume < d2.volume) {
return -1;
}
return 0;
});
setRecords(volumes);
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
}
}, [loading, selectedDrive, dispatch]);
const filteredRecords: IDirectPVVolumes[] = records.filter((elementItem) =>
elementItem.drive.includes(filter)
);
return (
<Fragment>
<PageHeader label="Volumes" />
<PageLayout>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Volumes"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
variant="standard"
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={[]}
columns={[
{
label: "Volume",
elementKey: "volume",
},
{
label: "Capacity",
elementKey: "capacity",
renderFunction: niceBytes,
},
{
label: "Node",
elementKey: "node",
},
{
label: "Drive",
elementKey: "drive",
},
]}
isLoading={loading}
records={filteredRecords}
entityName="Volumes"
idField="volume"
customPaperHeight={classes.tableWrapper}
/>
</Grid>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(DirectPVVolumes);

View File

@@ -0,0 +1,143 @@
// 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, useState } from "react";
import { DialogContentText, Grid, LinearProgress } from "@mui/material";
import { IDirectPVFormatResItem, IDirectPVFormatResult } from "./types";
import { ErrorResponseHandler } from "../../../common/types";
import api from "../../../common/api";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import PredefinedList from "../Common/FormComponents/PredefinedList/PredefinedList";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { FormatDrivesIcon } from "../../../icons";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
interface IFormatAllDrivesProps {
closeFormatModalAndRefresh: (
refresh: boolean,
formatIssuesList: IDirectPVFormatResItem[]
) => void;
deleteOpen: boolean;
allDrives: boolean;
drivesToFormat: string[];
}
const FormatDrives = ({
closeFormatModalAndRefresh,
deleteOpen,
allDrives,
drivesToFormat,
}: IFormatAllDrivesProps) => {
const dispatch = useAppDispatch();
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const [formatAll, setFormatAll] = useState<string>("");
const [force, setForce] = useState<boolean>(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
setDeleteLoading(true);
api
.invoke("POST", `/api/v1/directpv/drives/format`, {
drives: drivesToFormat,
force,
})
.then((res: IDirectPVFormatResult) => {
setDeleteLoading(false);
closeFormatModalAndRefresh(true, res.formatIssuesList);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
dispatch(setErrorSnackMessage(err));
});
};
return (
<ConfirmDialog
title={`Format ${allDrives ? "All " : ""} Drives`}
confirmText={`Format Drive${
drivesToFormat.length > 1 || allDrives ? "s" : ""
}`}
confirmButtonProps={{
disabled: formatAll !== "YES, PROCEED",
}}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={removeRecord}
onClose={() => {
closeFormatModalAndRefresh(false, []);
}}
titleIcon={<FormatDrivesIcon />}
confirmationContent={
<React.Fragment>
<DialogContentText>
{!allDrives && (
<Fragment>
<PredefinedList
label={`Selected Drive${
drivesToFormat.length > 1 ? "s" : ""
}`}
content={drivesToFormat.join(", ")}
/>
<br />
</Fragment>
)}
<Grid item xs={12}>
<FormSwitchWrapper
value="force"
id="force"
name="force"
checked={force}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setForce(event.target.checked);
}}
label={"Force Format"}
indicatorLabels={["Yes", "No"]}
/>
</Grid>
Are you sure you want to format{" "}
{allDrives ? <strong>All</strong> : "the selected"} drive
{drivesToFormat.length > 1 || allDrives ? "s" : ""}?.
<br />
<br />
<strong>
All information contained will be erased and cannot be recovered
</strong>
<br />
<br />
To continue please type <b>YES, PROCEED</b> in the box.
<Grid item xs={12}>
<InputBoxWrapper
id="format-confirm"
name="format-confirm"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setFormatAll(event.target.value);
}}
label=""
value={formatAll}
/>
</Grid>
</DialogContentText>
{deleteLoading && <LinearProgress />}
</React.Fragment>
}
/>
);
};
export default FormatDrives;

View File

@@ -0,0 +1,122 @@
// 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 { Button, Grid, Theme } from "@mui/material";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import React from "react";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { IDirectPVFormatResItem } from "./types";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import { DriveFormatErrorsIcon } from "../../../icons";
import { encodeURLString } from "../../../common/utils";
interface IFormatErrorsProps {
open: boolean;
onCloseFormatErrorsList: () => void;
errorsList: IDirectPVFormatResItem[];
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
errorsList: {
height: "calc(100vh - 280px)",
},
...modalStyleUtils,
});
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:application/json;charset=utf-8," + encodeURLString(text)
);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const FormatErrorsResult = ({
open,
onCloseFormatErrorsList,
errorsList,
classes,
}: IFormatErrorsProps) => {
return (
<ModalWrapper
modalOpen={open}
title={"Format Errors"}
onClose={onCloseFormatErrorsList}
titleIcon={<DriveFormatErrorsIcon />}
>
<Grid container>
<Grid item xs={12} className={classes.modalFormScrollable}>
There were some issues trying to format the selected CSI Drives,
please fix the issues and try again.
<br />
<TableWrapper
columns={[
{
label: "Node",
elementKey: "node",
},
{ label: "Drive", elementKey: "drive" },
{ label: "Message", elementKey: "error" },
]}
entityName="Format Errors"
idField="drive"
records={errorsList}
isLoading={false}
customPaperHeight={classes.errorsList}
textSelectable
noBackground
/>
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
color="primary"
variant="outlined"
onClick={() => {
download("csiFormatErrors.json", JSON.stringify([...errorsList]));
}}
>
Download
</Button>
<Button
onClick={onCloseFormatErrorsList}
color="primary"
variant="contained"
autoFocus
>
Done
</Button>
</Grid>
</Grid>
</ModalWrapper>
);
};
export default withStyles(styles)(FormatErrorsResult);

View File

@@ -0,0 +1,40 @@
// 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface IDirectPVReducer {
selectedDrive: string;
}
const initialState: IDirectPVReducer = {
selectedDrive: "",
};
export const directPVSlice = createSlice({
name: "directPV",
initialState,
reducers: {
selectDrive: (state, action: PayloadAction<string>) => {
if (action.payload !== "") {
state.selectedDrive = action.payload;
}
},
},
});
export const { selectDrive } = directPVSlice.actions;
export default directPVSlice.reducer;

View File

@@ -0,0 +1,50 @@
// 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/>.
export interface IDirectPVDrives {
joinName: string;
drive: string;
capacity: string;
allocated: string;
volumes: number;
node: string;
status: "Available" | "Unavailable" | "InUse" | "Ready" | "Terminating";
}
export interface IDirectPVVolumes {
volume: string;
capacity: string;
node: string;
drive: string;
}
export interface IDrivesResponse {
drives: IDirectPVDrives[];
}
export interface IVolumesResponse {
volumes: IDirectPVVolumes[];
}
export interface IDirectPVFormatResult {
formatIssuesList: IDirectPVFormatResItem[];
}
export interface IDirectPVFormatResItem {
node: string;
drive: string;
error: string;
}

View File

@@ -31,7 +31,12 @@ import api from "../../../common/api";
import MenuToggle from "./MenuToggle";
import ConsoleMenuList from "./ConsoleMenuList";
import { validRoutes } from "../valid-routes";
import { menuOpen, selOpMode, userLogged } from "../../../systemSlice";
import {
menuOpen,
selDirectPVMode,
selOpMode,
userLogged,
} from "../../../systemSlice";
import { resetSession, selFeatures } from "../consoleSlice";
const drawerWidth = 250;
@@ -97,6 +102,7 @@ const Menu = ({ classes }: IMenuProps) => {
(state: AppState) => state.system.sidebarOpen
);
const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode);
const logout = () => {
const deleteSession = () => {
@@ -117,7 +123,7 @@ const Menu = ({ classes }: IMenuProps) => {
deleteSession();
});
};
const allowedMenuItems = validRoutes(features, operatorMode);
const allowedMenuItems = validRoutes(features, operatorMode, directPVMode);
return (
<Drawer

View File

@@ -16,13 +16,19 @@
import React, { Fragment, Suspense, useEffect } from "react";
import OperatorLogo from "../../../icons/OperatorLogo";
import DirectPVLogo from "../../../icons/DirectPVLogo";
import { LoginMinIOLogo, VersionIcon } from "../../../icons";
import { Box, IconButton } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import LicensedConsoleLogo from "../Common/Components/LicensedConsoleLogo";
import { useSelector } from "react-redux";
import useApi from "../Common/Hooks/useApi";
import { setLicenseInfo } from "../../../systemSlice";
import {
selDirectPVMode,
selOpMode,
setLicenseInfo,
} from "../../../systemSlice";
import { AppState, useAppDispatch } from "../../../store";
import MenuToggleIcon from "../../../icons/MenuToggleIcon";
@@ -38,9 +44,9 @@ const MenuToggle = ({ isOpen, onToggle }: MenuToggleProps) => {
const licenseInfo = useSelector(
(state: AppState) => state?.system?.licenseInfo
);
const operatorMode = useSelector(
(state: AppState) => state.system.operatorMode
);
const operatorMode = useSelector(selOpMode);
const directPVMode = useSelector(selDirectPVMode);
const [isLicenseLoading, invokeLicenseInfoApi] = useApi(
(res: any) => {
@@ -107,9 +113,7 @@ const MenuToggle = ({ isOpen, onToggle }: MenuToggleProps) => {
>
{isOpen ? (
<div className={`logo ${stateClsName}`}>
{operatorMode ? (
<OperatorLogo />
) : (
{!operatorMode && !directPVMode ? (
<Fragment>
<div
style={{ marginLeft: "4px", width: 100, textAlign: "right" }}
@@ -122,6 +126,10 @@ const MenuToggle = ({ isOpen, onToggle }: MenuToggleProps) => {
/>
</div>
</Fragment>
) : (
<Fragment>
{directPVMode ? <DirectPVLogo /> : <OperatorLogo />}
</Fragment>
)}
</div>
) : (

View File

@@ -0,0 +1,167 @@
// 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 get from "lodash/get";
import { Theme } from "@mui/material/styles";
import { Grid } from "@mui/material";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import { setErrorSnackMessage } from "../../../systemSlice";
import { IPVCsResponse, IStoragePVCs } from "./types";
import api from "../../../common/api";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DeletePVC from "../Tenants/TenantDetails/DeletePVC";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import SearchBox from "../Common/SearchBox";
interface IStorageVolumesProps {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
tableWrapper: {
height: "calc(100vh - 150px)",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
const StorageVolumes = ({ classes }: IStorageVolumesProps) => {
const dispatch = useAppDispatch();
const [records, setRecords] = useState<IStoragePVCs[]>([]);
const [filter, setFilter] = useState("");
const [loading, setLoading] = useState<boolean>(true);
const [selectedPVC, setSelectedPVC] = useState<any>(null);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/list-pvcs`)
.then((res: IPVCsResponse) => {
let volumes = get(res, "pvcs", []);
setRecords(volumes ? volumes : []);
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
}
}, [loading, dispatch]);
const filteredRecords: IStoragePVCs[] = records.filter((elementItem) =>
elementItem.name.toLowerCase().includes(filter.toLowerCase())
);
const confirmDeletePVC = (pvcItem: IStoragePVCs) => {
const delPvc = {
...pvcItem,
tenant: pvcItem.tenant,
namespace: pvcItem.namespace,
};
setSelectedPVC(delPvc);
setDeleteOpen(true);
};
const tableActions = [{ type: "delete", onClick: confirmDeletePVC }];
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
setLoading(true);
};
return (
<Fragment>
{deleteOpen && (
<DeletePVC
deleteOpen={deleteOpen}
selectedPVC={selectedPVC}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader
label="Persistent Volumes Claims"
middleComponent={
<SearchBox
placeholder={"Search Volumes (PVCs)"}
onChange={(val) => {
setFilter(val);
}}
value={filter}
/>
}
/>
<PageLayout>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
},
{
label: "Namespace",
elementKey: "namespace",
width: 90,
},
{
label: "Status",
elementKey: "status",
width: 120,
},
{
label: "Tenant",
renderFullObject: true,
renderFunction: (record: any) =>
`${record.namespace}/${record.tenant}`,
},
{
label: "Capacity",
elementKey: "capacity",
width: 90,
},
{
label: "Storage Class",
elementKey: "storageClass",
},
]}
isLoading={loading}
records={filteredRecords}
entityName="PVCs"
idField="name"
customPaperHeight={classes.tableWrapper}
/>
</Grid>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(StorageVolumes);

View File

@@ -23,11 +23,12 @@ import { Bucket } from "./Buckets/types";
export const routesAsKbarActions = (
features: string[] | null,
operatorMode: boolean,
directPVMode: boolean,
buckets: Bucket[],
navigate: (url: string) => void
) => {
const initialActions: Action[] = [];
const allowedMenuItems = validRoutes(features, operatorMode);
const allowedMenuItems = validRoutes(features, operatorMode, directPVMode);
for (const i of allowedMenuItems) {
if (i.children && i.children.length > 0) {
for (const childI of i.children) {

View File

@@ -28,6 +28,7 @@ export interface ISessionResponse {
status: string;
features: string[];
operator: boolean;
directPV?: boolean;
distributedMode: boolean;
permissions: ISessionPermissions;
allowResources: IAllowResources[] | null;

View File

@@ -46,10 +46,12 @@ import { hasPermission } from "../../common/SecureComponent";
import WatchIcon from "../../icons/WatchIcon";
import RegisterMenuIcon from "../../icons/SidebarMenus/RegisterMenuIcon";
import {
ClustersIcon,
DocumentationIcon,
LambdaIcon,
LicenseIcon,
RecoverIcon,
StorageIcon,
TenantsOutlineIcon,
TiersIcon,
} from "../../icons";
@@ -59,7 +61,8 @@ import LicenseBadge from "./Menu/LicenseBadge";
export const validRoutes = (
features: string[] | null | undefined,
operatorMode: boolean
operatorMode: boolean,
directPVMode: boolean
) => {
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
let consoleMenus: IMenuItem[] = [
@@ -311,35 +314,101 @@ export const validRoutes = (
},
];
const allowedItems = (operatorMode ? operatorMenus : consoleMenus).filter(
(item: IMenuItem) => {
if (item.children && item.children.length > 0) {
const c = item.children?.filter((childItem: IMenuItem) => {
return (
((childItem.customPermissionFnc
? childItem.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[childItem.to ?? ""]
)) ||
childItem.forceDisplay) &&
!childItem.fsHidden
);
});
return c.length > 0;
}
let directPVMenus: IMenuItem[] = [
{
group: "Storage",
type: "item",
id: "StoragePVCs",
component: NavLink,
to: IAM_PAGES.DIRECTPV_STORAGE,
name: "PVCs",
icon: ClustersIcon,
forceDisplay: true,
},
{
name: "Drives",
type: "item",
id: "drives",
component: NavLink,
icon: DrivesMenuIcon,
to: IAM_PAGES.DIRECTPV_DRIVES,
forceDisplay: true,
},
{
name: "Volumes",
type: "item",
id: "volumes",
component: NavLink,
icon: StorageIcon,
to: IAM_PAGES.DIRECTPV_VOLUMES,
forceDisplay: true,
},
{
group: "DirectPV",
type: "item",
id: "License",
component: NavLink,
to: IAM_PAGES.LICENSE,
name: "License",
icon: LicenseIcon,
forceDisplay: true,
},
{
group: "DirectPV",
type: "item",
id: "Documentation",
component: NavLink,
to: IAM_PAGES.DOCUMENTATION,
name: "Documentation",
icon: DocumentationIcon,
forceDisplay: true,
onClick: (
e:
| React.MouseEvent<HTMLLIElement>
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement>
) => {
e.preventDefault();
window.open("https://docs.min.io/?ref=op", "_blank");
},
},
];
const res =
((item.customPermissionFnc
? item.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[item.to ?? ""]
)) ||
item.forceDisplay) &&
!item.fsHidden;
return res;
let menus = consoleMenus;
if (directPVMode) {
menus = directPVMenus;
} else if (operatorMode) {
menus = operatorMenus;
}
const allowedItems = menus.filter((item: IMenuItem) => {
if (item.children && item.children.length > 0) {
const c = item.children?.filter((childItem: IMenuItem) => {
return (
((childItem.customPermissionFnc
? childItem.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[childItem.to ?? ""]
)) ||
childItem.forceDisplay) &&
!childItem.fsHidden
);
});
return c.length > 0;
}
);
const res =
((item.customPermissionFnc
? item.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[item.to ?? ""]
)) ||
item.forceDisplay) &&
!item.fsHidden;
return res;
});
return allowedItems;
};

View File

@@ -50,6 +50,7 @@ import {
import { resetForm, setJwt } from "./loginSlice";
import StrategyForm from "./StrategyForm";
import { LoginField } from "./LoginField";
import DirectPVLogo from "../../icons/DirectPVLogo";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -260,6 +261,8 @@ const Login = () => {
);
const navigateTo = useSelector((state: AppState) => state.login.navigateTo);
const directPVMode = useSelector((state: AppState) => state.login.isDirectPV);
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
@@ -396,7 +399,13 @@ const Login = () => {
);
}
const consoleText = isOperator ? <OperatorLogo /> : <ConsoleLogo />;
let modeLogo = <ConsoleLogo />;
if (directPVMode) {
modeLogo = <DirectPVLogo />;
} else if (isOperator) {
modeLogo = <OperatorLogo />;
}
const hyperLink = isOperator
? "https://docs.min.io/minio/k8s/operator-console/operator-console.html?ref=con"
@@ -432,7 +441,7 @@ const Login = () => {
},
}}
>
<Box className={classes.iconLogo}>{consoleText}</Box>
<Box className={classes.iconLogo}>{modeLogo}</Box>
<Box
style={{
font: "normal normal normal 20px/24px Lato",

View File

@@ -37,6 +37,7 @@ export interface LoginState {
latestMinIOVersion: string;
loadingVersion: boolean;
isDirectPV: boolean;
navigateTo: string;
}
@@ -55,6 +56,7 @@ const initialState: LoginState = {
loadingFetchConfiguration: true,
latestMinIOVersion: "",
loadingVersion: true,
isDirectPV: false,
navigateTo: "",
};
@@ -107,6 +109,7 @@ export const loginSlice = createSlice({
state.loadingFetchConfiguration = false;
if (action.payload) {
state.loginStrategy = action.payload;
state.isDirectPV = !!action.payload.isDirectPV;
}
})
.addCase(doLoginAsync.pending, (state, action) => {

View File

@@ -17,6 +17,7 @@
export interface ILoginDetails {
loginStrategy: loginStrategyType;
redirect: string;
isDirectPV?: boolean;
}
export enum loginStrategyType {

View File

@@ -35,6 +35,7 @@ import editPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/EditP
import editTenantMonitoringReducer from "./screens/Console/Tenants/TenantDetails/tenantMonitoringSlice";
import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetails/tenantAuditLogSlice";
import editTenantSecurityContextReducer from "./screens/Console/Tenants/tenantSecurityContextSlice";
import directPVReducer from "./screens/Console/DirectPV/directPVSlice";
const rootReducer = combineReducers({
system: systemReducer,
@@ -57,6 +58,7 @@ const rootReducer = combineReducers({
editTenantMonitoring: editTenantMonitoringReducer,
editTenantLogging: editTenantAuditLoggingReducer,
editTenantSecurityContext: editTenantSecurityContextReducer,
directPV: directPVReducer,
});
export const store = configureStore({

View File

@@ -29,6 +29,7 @@ export interface SystemState {
loggedIn: boolean;
showMarketplace: boolean;
operatorMode: boolean;
directPVMode: boolean;
sidebarOpen: boolean;
session: string;
userName: string;
@@ -48,6 +49,7 @@ const initialState: SystemState = {
loggedIn: false,
showMarketplace: false,
operatorMode: false,
directPVMode: false,
session: "",
userName: "",
sidebarOpen: initSideBarOpen,
@@ -83,6 +85,9 @@ export const systemSlice = createSlice({
operatorMode: (state, action: PayloadAction<boolean>) => {
state.operatorMode = action.payload;
},
directPVMode: (state, action: PayloadAction<boolean>) => {
state.directPVMode = action.payload;
},
menuOpen: (state, action: PayloadAction<boolean>) => {
// persist preference to local storage
localStorage.setItem(
@@ -154,6 +159,7 @@ export const {
userLogged,
showMarketplace,
operatorMode,
directPVMode,
menuOpen,
setServerNeedsRestart,
serverIsLoading,
@@ -171,6 +177,7 @@ export const {
export const selDistSet = (state: AppState) => state.system.distributedSetup;
export const selSiteRep = (state: AppState) => state.system.siteReplicationInfo;
export const selOpMode = (state: AppState) => state.system.operatorMode;
export const selDirectPVMode = (state: AppState) => state.system.directPVMode;
export const selShowMarketplace = (state: AppState) =>
state.system.showMarketplace;

View File

@@ -1501,6 +1501,8 @@ definitions:
enum: [ form, redirect, service-account, redirect-service-account ]
redirect:
type: string
isDirectPV:
type: boolean
loginRequest:
type: object
properties:
@@ -1544,6 +1546,8 @@ definitions:
enum: [ ok ]
operator:
type: boolean
directPV:
type: boolean
permissions:
type: object
additionalProperties: