Move Register Component to Redux (#2630)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -30,11 +30,12 @@ import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
|
||||
|
||||
interface IApiKeyRegister {
|
||||
classes: any;
|
||||
registerEndpoint: string;
|
||||
afterRegister: () => void;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -45,11 +46,9 @@ const styles = (theme: Theme) =>
|
||||
...spacingUtils,
|
||||
});
|
||||
|
||||
const ApiKeyRegister = ({
|
||||
classes,
|
||||
registerEndpoint,
|
||||
afterRegister,
|
||||
}: IApiKeyRegister) => {
|
||||
const ApiKeyRegister = ({ classes, registerEndpoint }: IApiKeyRegister) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -67,8 +66,7 @@ const ApiKeyRegister = ({
|
||||
.then((resp: SubnetLoginResponse) => {
|
||||
setLoading(false);
|
||||
if (resp && resp.registered) {
|
||||
reset();
|
||||
afterRegister();
|
||||
navigate(IAM_PAGES.LICENSE);
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
@@ -76,7 +74,7 @@ const ApiKeyRegister = ({
|
||||
setLoading(false);
|
||||
reset();
|
||||
});
|
||||
}, [afterRegister, apiKey, dispatch, loading, registerEndpoint]);
|
||||
}, [apiKey, dispatch, loading, registerEndpoint, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fromModal) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { FormTitle } from "./utils";
|
||||
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
import { setLoading, setSelectedSubnetOrganization } from "./registerSlice";
|
||||
import { Button } from "mds";
|
||||
import RegisterHelpBox from "./RegisterHelpBox";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import { callRegister } from "./registerThunks";
|
||||
|
||||
const ClusterRegistrationForm = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const subnetAccessToken = useSelector(
|
||||
(state: AppState) => state.register.subnetAccessToken
|
||||
);
|
||||
const selectedSubnetOrganization = useSelector(
|
||||
(state: AppState) => state.register.selectedSubnetOrganization
|
||||
);
|
||||
const subnetOrganizations = useSelector(
|
||||
(state: AppState) => state.register.subnetOrganizations
|
||||
);
|
||||
const loading = useSelector((state: AppState) => state.register.loading);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
"& .title-text": {
|
||||
marginLeft: "0px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle title={`Register MinIO cluster`} />
|
||||
</Box>
|
||||
<Box>
|
||||
<SelectWrapper
|
||||
id="subnet-organization"
|
||||
name="subnet-organization"
|
||||
onChange={(e) =>
|
||||
dispatch(setSelectedSubnetOrganization(e.target.value as string))
|
||||
}
|
||||
label="Select an organization"
|
||||
value={selectedSubnetOrganization}
|
||||
options={subnetOrganizations.map((organization) => ({
|
||||
label: organization.company,
|
||||
value: organization.accountId.toString(),
|
||||
}))}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"register-cluster"}
|
||||
onClick={() => () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
dispatch(setLoading(true));
|
||||
if (subnetAccessToken && selectedSubnetOrganization) {
|
||||
dispatch(
|
||||
callRegister({
|
||||
token: subnetAccessToken,
|
||||
account_id: selectedSubnetOrganization,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={loading || subnetAccessToken.trim().length === 0}
|
||||
variant="callAction"
|
||||
label={"Register"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterRegistrationForm;
|
||||
159
portal-ui/src/screens/Console/Support/OfflineRegistration.tsx
Normal file
159
portal-ui/src/screens/Console/Support/OfflineRegistration.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
import { Box, Link } from "@mui/material";
|
||||
import { ClusterRegistered, FormTitle } from "./utils";
|
||||
import { Button, CopyIcon, OfflineRegistrationIcon } from "mds";
|
||||
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import RegisterHelpBox from "./RegisterHelpBox";
|
||||
import { AppState } from "../../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const OfflineRegistration = () => {
|
||||
const subnetRegToken = useSelector(
|
||||
(state: AppState) => state.register.subnetRegToken
|
||||
);
|
||||
const clusterRegistered = useSelector(
|
||||
(state: AppState) => state.register.clusterRegistered
|
||||
);
|
||||
const licenseInfo = useSelector(
|
||||
(state: AppState) => state.register.licenseInfo
|
||||
);
|
||||
|
||||
const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid #eaeaea",
|
||||
borderRadius: "2px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
padding: "43px",
|
||||
}}
|
||||
>
|
||||
{clusterRegistered && licenseInfo ? (
|
||||
<ClusterRegistered email={licenseInfo.email} />
|
||||
) : null}
|
||||
<Box
|
||||
sx={{
|
||||
"& .title-text": {
|
||||
marginLeft: "27px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle
|
||||
icon={<OfflineRegistrationIcon />}
|
||||
title={`Register cluster in an Air-gap environment`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
marginTop: "15px",
|
||||
"& .step-number": {
|
||||
color: "#ffffff",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
background: "#081C42",
|
||||
marginRight: "10px",
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
|
||||
"& .step-row": {
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box className="step-row">
|
||||
<div className="step-text">
|
||||
Click on the link to register this cluster in SUBNET
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
style={{
|
||||
color: "#2781B0",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
color="inherit"
|
||||
href={offlineRegUrl}
|
||||
target="_blank"
|
||||
>
|
||||
https://subnet.min.io/cluster/register
|
||||
</Link>
|
||||
|
||||
<TooltipWrapper tooltip={"Copy to Clipboard"}>
|
||||
<CopyToClipboard text={offlineRegUrl}>
|
||||
<Button
|
||||
type={"button"}
|
||||
id={"copy-ult-to-clip-board"}
|
||||
icon={<CopyIcon />}
|
||||
color={"primary"}
|
||||
variant={"regular"}
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</TooltipWrapper>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "25px",
|
||||
fontSize: "14px",
|
||||
fontStyle: "italic",
|
||||
color: "#5E5E5E",
|
||||
}}
|
||||
>
|
||||
If this machine does not have internet connection, Copy paste
|
||||
the following URL in a browser where you access SUBNET and
|
||||
follow the instructions to complete the registration
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
</Box>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default OfflineRegistration;
|
||||
190
portal-ui/src/screens/Console/Support/OnlineRegistration.tsx
Normal file
190
portal-ui/src/screens/Console/Support/OnlineRegistration.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { FormTitle } from "./utils";
|
||||
import { Button, OnlineRegistrationIcon, UsersIcon } from "mds";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
|
||||
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
|
||||
import RegisterHelpBox from "./RegisterHelpBox";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selOpMode } from "../../../systemSlice";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import {
|
||||
setShowPassword,
|
||||
setSubnetEmail,
|
||||
setSubnetPassword,
|
||||
} from "./registerSlice";
|
||||
import { subnetLogin } from "./registerThunks";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
sizedLabel: {
|
||||
minWidth: "75px",
|
||||
},
|
||||
...spacingUtils,
|
||||
})
|
||||
);
|
||||
|
||||
const OnlineRegistration = () => {
|
||||
const classes = useStyles();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const operatorMode = useSelector(selOpMode);
|
||||
const subnetPassword = useSelector(
|
||||
(state: AppState) => state.register.subnetPassword
|
||||
);
|
||||
const subnetEmail = useSelector(
|
||||
(state: AppState) => state.register.subnetEmail
|
||||
);
|
||||
const showPassword = useSelector(
|
||||
(state: AppState) => state.register.showPassword
|
||||
);
|
||||
const loading = useSelector((state: AppState) => state.register.loading);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
"& .title-text": {
|
||||
marginLeft: "27px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle
|
||||
icon={<OnlineRegistrationIcon />}
|
||||
title={`Online activation of MinIO Subscription Network License`}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: {
|
||||
xs: "column",
|
||||
md: "row",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
marginTop: "30px",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
Use your MinIO Subscription Network login credentials to register
|
||||
this cluster.
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
className={classes.spacerBottom}
|
||||
classes={{
|
||||
inputLabel: classes.sizedLabel,
|
||||
}}
|
||||
id="subnet-email"
|
||||
name="subnet-email"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSubnetEmail(event.target.value))
|
||||
}
|
||||
label="Email"
|
||||
value={subnetEmail}
|
||||
overlayIcon={<UsersIcon />}
|
||||
/>
|
||||
<InputBoxWrapper
|
||||
className={classes.spacerBottom}
|
||||
classes={{
|
||||
inputLabel: classes.sizedLabel,
|
||||
}}
|
||||
id="subnet-password"
|
||||
name="subnet-password"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSubnetPassword(event.target.value))
|
||||
}
|
||||
label="Password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={subnetPassword}
|
||||
overlayIcon={
|
||||
showPassword ? <VisibilityOffIcon /> : <RemoveRedEyeIcon />
|
||||
}
|
||||
overlayAction={() => dispatch(setShowPassword(!showPassword))}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
"& button": {
|
||||
marginLeft: "8px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"sign-up"}
|
||||
type="submit"
|
||||
className={classes.spacerRight}
|
||||
variant="regular"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(
|
||||
`https://min.io/signup?ref=${operatorMode ? "op" : "con"}`,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
label={"Sign up"}
|
||||
/>
|
||||
<Button
|
||||
id={"register-credentials"}
|
||||
type="submit"
|
||||
variant="callAction"
|
||||
disabled={
|
||||
loading ||
|
||||
subnetEmail.trim().length === 0 ||
|
||||
subnetPassword.trim().length === 0
|
||||
}
|
||||
onClick={() => dispatch(subnetLogin())}
|
||||
label={"Register"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnlineRegistration;
|
||||
@@ -14,61 +14,37 @@
|
||||
// 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, useCallback, useEffect, useState } from "react";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import {
|
||||
Button,
|
||||
CopyIcon,
|
||||
OfflineRegistrationIcon,
|
||||
OnlineRegistrationIcon,
|
||||
PageHeader,
|
||||
UsersIcon,
|
||||
} from "mds";
|
||||
import { PageHeader } from "mds";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
spacingUtils,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { Box, Link } from "@mui/material";
|
||||
import { Box } from "@mui/material";
|
||||
import PageLayout from "../Common/Layout/PageLayout";
|
||||
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
|
||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import api from "../../../common/api";
|
||||
|
||||
import {
|
||||
SubnetInfo,
|
||||
SubnetLoginRequest,
|
||||
SubnetLoginResponse,
|
||||
SubnetLoginWithMFARequest,
|
||||
SubnetOrganization,
|
||||
SubnetRegisterRequest,
|
||||
SubnetRegTokenResponse,
|
||||
} from "../License/types";
|
||||
import { SubnetRegTokenResponse } from "../License/types";
|
||||
import { ErrorResponseHandler } from "../../../common/types";
|
||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
import { hasPermission } from "../../../common/SecureComponent";
|
||||
import {
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES,
|
||||
IAM_PAGES_PERMISSIONS,
|
||||
} from "../../../common/SecureComponent/permissions";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import RegisterHelpBox from "./RegisterHelpBox";
|
||||
import { selOpMode, setErrorSnackMessage } from "../../../systemSlice";
|
||||
import { useAppDispatch } from "../../../store";
|
||||
import { setErrorSnackMessage } from "../../../systemSlice";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import { TabPanel } from "../../shared/tabs";
|
||||
import { ClusterRegistered, FormTitle, ProxyConfiguration } from "./utils";
|
||||
import { ClusterRegistered, ProxyConfiguration } from "./utils";
|
||||
import ApiKeyRegister from "./ApiKeyRegister";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
||||
import { fetchLicenseInfo } from "./registerThunks";
|
||||
import {
|
||||
resetRegisterForm,
|
||||
setCurTab,
|
||||
setLoading,
|
||||
setSubnetRegToken,
|
||||
} from "./registerSlice";
|
||||
import OfflineRegistration from "./OfflineRegistration";
|
||||
import SubnetMFAToken from "./SubnetMFAToken";
|
||||
import ClusterRegistrationForm from "./ClusterRegistrationForm";
|
||||
import OnlineRegistration from "./OnlineRegistration";
|
||||
|
||||
interface IRegister {
|
||||
classes: any;
|
||||
@@ -79,469 +55,83 @@ const styles = (theme: Theme) =>
|
||||
sizedLabel: {
|
||||
minWidth: "75px",
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...spacingUtils,
|
||||
...containerForHeader,
|
||||
});
|
||||
|
||||
const Register = ({ classes }: IRegister) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const operatorMode = useSelector(selOpMode);
|
||||
const [license, setLicense] = useState<string>("");
|
||||
const [subnetPassword, setSubnetPassword] = useState<string>("");
|
||||
const [subnetEmail, setSubnetEmail] = useState<string>("");
|
||||
const [subnetMFAToken, setSubnetMFAToken] = useState<string>("");
|
||||
const [subnetOTP, setSubnetOTP] = useState<string>("");
|
||||
const [subnetAccessToken, setSubnetAccessToken] = useState<string>("");
|
||||
const [selectedSubnetOrganization, setSelectedSubnetOrganization] =
|
||||
useState<string>("");
|
||||
const [subnetRegToken, setSubnetRegToken] = useState<string>("");
|
||||
const [subnetOrganizations, setSubnetOrganizations] = useState<
|
||||
SubnetOrganization[]
|
||||
>([]);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [loadingLicenseInfo, setLoadingLicenseInfo] = useState<boolean>(false);
|
||||
const [clusterRegistered, setClusterRegistered] = useState<boolean>(false);
|
||||
const [licenseInfo, setLicenseInfo] = useState<SubnetInfo | undefined>();
|
||||
const [curTab, setCurTab] = useState<number>(0);
|
||||
|
||||
const subnetMFAToken = useSelector(
|
||||
(state: AppState) => state.register.subnetMFAToken
|
||||
);
|
||||
const subnetAccessToken = useSelector(
|
||||
(state: AppState) => state.register.subnetAccessToken
|
||||
);
|
||||
|
||||
const subnetRegToken = useSelector(
|
||||
(state: AppState) => state.register.subnetRegToken
|
||||
);
|
||||
const subnetOrganizations = useSelector(
|
||||
(state: AppState) => state.register.subnetOrganizations
|
||||
);
|
||||
|
||||
const loading = useSelector((state: AppState) => state.register.loading);
|
||||
const loadingLicenseInfo = useSelector(
|
||||
(state: AppState) => state.register.loadingLicenseInfo
|
||||
);
|
||||
const clusterRegistered = useSelector(
|
||||
(state: AppState) => state.register.clusterRegistered
|
||||
);
|
||||
const licenseInfo = useSelector(
|
||||
(state: AppState) => state.register.licenseInfo
|
||||
);
|
||||
const curTab = useSelector((state: AppState) => state.register.curTab);
|
||||
|
||||
const [initialLicenseLoading, setInitialLicenseLoading] =
|
||||
useState<boolean>(true);
|
||||
const clearForm = () => {
|
||||
setSubnetAccessToken("");
|
||||
setSelectedSubnetOrganization("");
|
||||
setSubnetRegToken("");
|
||||
setShowPassword(false);
|
||||
setSubnetOrganizations([]);
|
||||
setLicense("");
|
||||
setSubnetPassword("");
|
||||
setSubnetEmail("");
|
||||
setSubnetMFAToken("");
|
||||
setSubnetOTP("");
|
||||
};
|
||||
|
||||
const getSubnetInfo = hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
|
||||
true
|
||||
);
|
||||
|
||||
const fetchLicenseInfo = useCallback(() => {
|
||||
if (loadingLicenseInfo) {
|
||||
return;
|
||||
}
|
||||
if (getSubnetInfo) {
|
||||
setLoadingLicenseInfo(true);
|
||||
api
|
||||
.invoke("GET", `/api/v1/subnet/info`)
|
||||
.then((res: SubnetInfo) => {
|
||||
setLicenseInfo(res);
|
||||
setClusterRegistered(true);
|
||||
setLoadingLicenseInfo(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
if (
|
||||
err.detailedError.toLowerCase() !==
|
||||
"License is not present".toLowerCase()
|
||||
) {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
}
|
||||
setClusterRegistered(false);
|
||||
setLoadingLicenseInfo(false);
|
||||
});
|
||||
} else {
|
||||
setLoadingLicenseInfo(false);
|
||||
}
|
||||
}, [loadingLicenseInfo, getSubnetInfo, dispatch]);
|
||||
useEffect(() => {
|
||||
// when unmounted, reset
|
||||
return () => {
|
||||
dispatch(resetRegisterForm());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const fetchSubnetRegToken = () => {
|
||||
if (loading || subnetRegToken) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
dispatch(setLoading(true));
|
||||
api
|
||||
.invoke("GET", "/api/v1/subnet/registration-token")
|
||||
.then((resp: SubnetRegTokenResponse) => {
|
||||
setLoading(false);
|
||||
dispatch(setLoading(false));
|
||||
if (resp && resp.regToken) {
|
||||
setSubnetRegToken(resp.regToken);
|
||||
dispatch(setSubnetRegToken(resp.regToken));
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(err);
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const callRegister = (token: string, account_id: string) => {
|
||||
const request: SubnetRegisterRequest = {
|
||||
token: token,
|
||||
account_id: account_id,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/register", request)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
clearForm();
|
||||
fetchLicenseInfo();
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
const subnetRegister = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
if (subnetAccessToken && selectedSubnetOrganization) {
|
||||
callRegister(subnetAccessToken, selectedSubnetOrganization);
|
||||
}
|
||||
};
|
||||
|
||||
const subnetLoginWithMFA = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const request: SubnetLoginWithMFARequest = {
|
||||
username: subnetEmail,
|
||||
otp: subnetOTP,
|
||||
mfa_token: subnetMFAToken,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/login/mfa", request)
|
||||
.then((resp: SubnetLoginResponse) => {
|
||||
setLoading(false);
|
||||
if (resp && resp.access_token && resp.organizations.length > 0) {
|
||||
if (resp.organizations.length === 1) {
|
||||
callRegister(
|
||||
resp.access_token,
|
||||
resp.organizations[0].accountId.toString()
|
||||
);
|
||||
} else {
|
||||
setSubnetAccessToken(resp.access_token);
|
||||
setSubnetOrganizations(resp.organizations);
|
||||
setSelectedSubnetOrganization(
|
||||
resp.organizations[0].accountId.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoading(false);
|
||||
setSubnetOTP("");
|
||||
});
|
||||
};
|
||||
|
||||
const subnetLogin = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
let request: SubnetLoginRequest = {
|
||||
username: subnetEmail,
|
||||
password: subnetPassword,
|
||||
apiKey: license,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/login", request)
|
||||
.then((resp: SubnetLoginResponse) => {
|
||||
setLoading(false);
|
||||
if (resp && resp.registered) {
|
||||
clearForm();
|
||||
fetchLicenseInfo();
|
||||
} else if (resp && resp.mfa_token) {
|
||||
setSubnetMFAToken(resp.mfa_token);
|
||||
} else if (resp && resp.access_token && resp.organizations.length > 0) {
|
||||
setSubnetAccessToken(resp.access_token);
|
||||
setSubnetOrganizations(resp.organizations);
|
||||
setSelectedSubnetOrganization(
|
||||
resp.organizations[0].accountId.toString()
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoading(false);
|
||||
clearForm();
|
||||
dispatch(setLoading(false));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLicenseLoading) {
|
||||
fetchLicenseInfo();
|
||||
dispatch(fetchLicenseInfo());
|
||||
setInitialLicenseLoading(false);
|
||||
}
|
||||
}, [fetchLicenseInfo, initialLicenseLoading, setInitialLicenseLoading]);
|
||||
}, [initialLicenseLoading, setInitialLicenseLoading, dispatch]);
|
||||
|
||||
let clusterRegistrationForm: JSX.Element = <Fragment />;
|
||||
|
||||
if (subnetAccessToken && subnetOrganizations.length > 0) {
|
||||
clusterRegistrationForm = (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
"& .title-text": {
|
||||
marginLeft: "0px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle title={`Register MinIO cluster`} />
|
||||
</Box>
|
||||
<Box>
|
||||
<SelectWrapper
|
||||
id="subnet-organization"
|
||||
name="subnet-organization"
|
||||
onChange={(e) =>
|
||||
setSelectedSubnetOrganization(e.target.value as string)
|
||||
}
|
||||
label="Select an organization"
|
||||
value={selectedSubnetOrganization}
|
||||
options={subnetOrganizations.map((organization) => ({
|
||||
label: organization.company,
|
||||
value: organization.accountId.toString(),
|
||||
}))}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"register-cluster"}
|
||||
onClick={() => subnetRegister()}
|
||||
disabled={loading || subnetAccessToken.trim().length === 0}
|
||||
variant="callAction"
|
||||
label={"Register"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
);
|
||||
clusterRegistrationForm = <ClusterRegistrationForm />;
|
||||
} else if (subnetMFAToken) {
|
||||
clusterRegistrationForm = (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
marginTop: "30px",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
Two-Factor Authentication
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
Please enter the 6-digit verification code that was sent to your
|
||||
email address. This code will be valid for 5 minutes.
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
overlayIcon={<LockOutlinedIcon />}
|
||||
id="subnet-otp"
|
||||
name="subnet-otp"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSubnetOTP(event.target.value)
|
||||
}
|
||||
placeholder=""
|
||||
label=""
|
||||
value={subnetOTP}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"verify"}
|
||||
onClick={() => subnetLoginWithMFA()}
|
||||
disabled={
|
||||
loading ||
|
||||
subnetOTP.trim().length === 0 ||
|
||||
subnetMFAToken.trim().length === 0
|
||||
}
|
||||
variant="callAction"
|
||||
label={"Verify"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
);
|
||||
clusterRegistrationForm = <SubnetMFAToken />;
|
||||
} else {
|
||||
clusterRegistrationForm = (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
"& .title-text": {
|
||||
marginLeft: "27px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle
|
||||
icon={<OnlineRegistrationIcon />}
|
||||
title={`Online activation of MinIO Subscription Network License`}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: {
|
||||
xs: "column",
|
||||
md: "row",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
marginTop: "30px",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
Use your MinIO Subscription Network login credentials to register
|
||||
this cluster.
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
className={classes.spacerBottom}
|
||||
classes={{
|
||||
inputLabel: classes.sizedLabel,
|
||||
}}
|
||||
id="subnet-email"
|
||||
name="subnet-email"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSubnetEmail(event.target.value)
|
||||
}
|
||||
label="Email"
|
||||
value={subnetEmail}
|
||||
overlayIcon={<UsersIcon />}
|
||||
/>
|
||||
<InputBoxWrapper
|
||||
className={classes.spacerBottom}
|
||||
classes={{
|
||||
inputLabel: classes.sizedLabel,
|
||||
}}
|
||||
id="subnet-password"
|
||||
name="subnet-password"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSubnetPassword(event.target.value)
|
||||
}
|
||||
label="Password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={subnetPassword}
|
||||
overlayIcon={
|
||||
showPassword ? <VisibilityOffIcon /> : <RemoveRedEyeIcon />
|
||||
}
|
||||
overlayAction={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
"& button": {
|
||||
marginLeft: "8px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"sign-up"}
|
||||
type="submit"
|
||||
className={classes.spacerRight}
|
||||
variant="regular"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(
|
||||
`https://min.io/signup?ref=${
|
||||
operatorMode ? "op" : "con"
|
||||
}`,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
label={"Sign up"}
|
||||
/>
|
||||
<Button
|
||||
id={"register-credentials"}
|
||||
type="submit"
|
||||
variant="callAction"
|
||||
disabled={
|
||||
loading ||
|
||||
subnetEmail.trim().length === 0 ||
|
||||
subnetPassword.trim().length === 0
|
||||
}
|
||||
onClick={() => subnetLogin()}
|
||||
label={"Register"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
</Fragment>
|
||||
);
|
||||
clusterRegistrationForm = <OnlineRegistration />;
|
||||
}
|
||||
|
||||
const apiKeyRegistration = (
|
||||
@@ -558,131 +148,14 @@ const Register = ({ classes }: IRegister) => {
|
||||
{clusterRegistered && licenseInfo ? (
|
||||
<ClusterRegistered email={licenseInfo.email} />
|
||||
) : (
|
||||
<ApiKeyRegister
|
||||
afterRegister={fetchLicenseInfo}
|
||||
registerEndpoint={"/api/v1/subnet/login"}
|
||||
/>
|
||||
<ApiKeyRegister registerEndpoint={"/api/v1/subnet/login"} />
|
||||
)}
|
||||
</Box>
|
||||
<ProxyConfiguration linkClass={classes.link} />
|
||||
<ProxyConfiguration />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
|
||||
const offlineRegistration = (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid #eaeaea",
|
||||
borderRadius: "2px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
padding: "43px",
|
||||
}}
|
||||
>
|
||||
{clusterRegistered && licenseInfo ? (
|
||||
<ClusterRegistered email={licenseInfo.email} />
|
||||
) : null}
|
||||
<Box
|
||||
sx={{
|
||||
"& .title-text": {
|
||||
marginLeft: "27px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormTitle
|
||||
icon={<OfflineRegistrationIcon />}
|
||||
title={`Register cluster in an Airgap environment`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
marginTop: "15px",
|
||||
"& .step-number": {
|
||||
color: "#ffffff",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
background: "#081C42",
|
||||
marginRight: "10px",
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
|
||||
"& .step-row": {
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box className="step-row">
|
||||
<div className="step-text">
|
||||
Click on the link to register this cluster in SUBNET
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className={classes.link}
|
||||
color="inherit"
|
||||
href={offlineRegUrl}
|
||||
target="_blank"
|
||||
>
|
||||
https://subnet.min.io/cluster/register
|
||||
</Link>
|
||||
|
||||
<TooltipWrapper tooltip={"Copy to Clipboard"}>
|
||||
<CopyToClipboard text={offlineRegUrl}>
|
||||
<Button
|
||||
type={"button"}
|
||||
id={"copy-ult-to-clip-board"}
|
||||
icon={<CopyIcon />}
|
||||
color={"primary"}
|
||||
variant={"regular"}
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</TooltipWrapper>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "25px",
|
||||
fontSize: "14px",
|
||||
fontStyle: "italic",
|
||||
color: "#5E5E5E",
|
||||
}}
|
||||
>
|
||||
If this machine does not have internet connection, Copy paste
|
||||
the following URL in a browser where you access SUBNET and
|
||||
follow the instructions to complete the registration
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
</Box>
|
||||
</Fragment>
|
||||
);
|
||||
const offlineRegistration = <OfflineRegistration />;
|
||||
|
||||
const regUi = (
|
||||
<Fragment>
|
||||
@@ -702,7 +175,7 @@ const Register = ({ classes }: IRegister) => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!clusterRegistered && <ProxyConfiguration linkClass={classes.link} />}
|
||||
{!clusterRegistered && <ProxyConfiguration />}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -720,7 +193,7 @@ const Register = ({ classes }: IRegister) => {
|
||||
<Tabs
|
||||
value={curTab}
|
||||
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setCurTab(newValue);
|
||||
dispatch(setCurTab(newValue));
|
||||
}}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
@@ -731,17 +204,17 @@ const Register = ({ classes }: IRegister) => {
|
||||
<Tab
|
||||
label="Credentials"
|
||||
id="simple-tab-0"
|
||||
aria-controls="simple-tabpanel-0"
|
||||
aria-controls="simple-tab-panel-0"
|
||||
/>
|
||||
<Tab
|
||||
label="API Key"
|
||||
id="simple-tab-1"
|
||||
aria-controls="simple-tabpanel-1"
|
||||
aria-controls="simple-tab-panel-1"
|
||||
/>
|
||||
<Tab
|
||||
label="Airgap"
|
||||
label="Air-Gap"
|
||||
id="simple-tab-2"
|
||||
aria-controls="simple-tabpanel-2"
|
||||
aria-controls="simple-tab-panel-2"
|
||||
onClick={() => fetchSubnetRegToken()}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
@@ -82,10 +82,7 @@ const RegisterOperator = ({ classes }: IRegister) => {
|
||||
{apiKeyRegistered ? (
|
||||
<ClusterRegistered email={"Operator"} />
|
||||
) : (
|
||||
<ApiKeyRegister
|
||||
registerEndpoint={"/api/v1/subnet/apikey/register"}
|
||||
afterRegister={fetchAPIKeyInfo}
|
||||
/>
|
||||
<ApiKeyRegister registerEndpoint={"/api/v1/subnet/apikey/register"} />
|
||||
)}
|
||||
</Box>
|
||||
</Fragment>
|
||||
|
||||
@@ -78,7 +78,10 @@ function RegisterStatus({ classes, showHelp }: IRegisterStatus) {
|
||||
<Link
|
||||
href="https://subnet.min.io"
|
||||
target="_blank"
|
||||
className={classes.link}
|
||||
style={{
|
||||
color: "#2781B0",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
SUBNET
|
||||
</Link>{" "}
|
||||
|
||||
110
portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx
Normal file
110
portal-ui/src/screens/Console/Support/SubnetMFAToken.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||
import { setSubnetOTP } from "./registerSlice";
|
||||
import { Button } from "mds";
|
||||
import RegisterHelpBox from "./RegisterHelpBox";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { subnetLoginWithMFA } from "./registerThunks";
|
||||
|
||||
const SubnetMFAToken = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const subnetMFAToken = useSelector(
|
||||
(state: AppState) => state.register.subnetMFAToken
|
||||
);
|
||||
const subnetOTP = useSelector((state: AppState) => state.register.subnetOTP);
|
||||
const loading = useSelector((state: AppState) => state.register.loading);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
marginTop: "30px",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
Two-Factor Authentication
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
Please enter the 6-digit verification code that was sent to your email
|
||||
address. This code will be valid for 5 minutes.
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
overlayIcon={<LockOutlinedIcon />}
|
||||
id="subnet-otp"
|
||||
name="subnet-otp"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSubnetOTP(event.target.value))
|
||||
}
|
||||
placeholder=""
|
||||
label=""
|
||||
value={subnetOTP}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
id={"verify"}
|
||||
onClick={() => dispatch(subnetLoginWithMFA())}
|
||||
disabled={
|
||||
loading ||
|
||||
subnetOTP.trim().length === 0 ||
|
||||
subnetMFAToken.trim().length === 0
|
||||
}
|
||||
variant="callAction"
|
||||
label={"Verify"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<RegisterHelpBox />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default SubnetMFAToken;
|
||||
132
portal-ui/src/screens/Console/Support/registerSlice.ts
Normal file
132
portal-ui/src/screens/Console/Support/registerSlice.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 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";
|
||||
import { SubnetInfo, SubnetOrganization } from "../License/types";
|
||||
|
||||
export interface RegisterState {
|
||||
license: string;
|
||||
subnetPassword: string;
|
||||
subnetEmail: string;
|
||||
subnetMFAToken: string;
|
||||
subnetOTP: string;
|
||||
subnetAccessToken: string;
|
||||
selectedSubnetOrganization: string;
|
||||
subnetRegToken: string;
|
||||
subnetOrganizations: SubnetOrganization[];
|
||||
showPassword: boolean;
|
||||
loading: boolean;
|
||||
loadingLicenseInfo: boolean;
|
||||
clusterRegistered: boolean;
|
||||
licenseInfo: SubnetInfo | undefined;
|
||||
curTab: number;
|
||||
}
|
||||
|
||||
const initialState: RegisterState = {
|
||||
license: "",
|
||||
subnetPassword: "",
|
||||
subnetEmail: "",
|
||||
subnetMFAToken: "",
|
||||
subnetOTP: "",
|
||||
subnetAccessToken: "",
|
||||
selectedSubnetOrganization: "",
|
||||
subnetRegToken: "",
|
||||
subnetOrganizations: [],
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
loadingLicenseInfo: false,
|
||||
clusterRegistered: false,
|
||||
licenseInfo: undefined,
|
||||
curTab: 0,
|
||||
};
|
||||
|
||||
export const registerSlice = createSlice({
|
||||
name: "register",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLicense: (state, action: PayloadAction<string>) => {
|
||||
state.license = action.payload;
|
||||
},
|
||||
setSubnetPassword: (state, action: PayloadAction<string>) => {
|
||||
state.subnetPassword = action.payload;
|
||||
},
|
||||
setSubnetEmail: (state, action: PayloadAction<string>) => {
|
||||
state.subnetEmail = action.payload;
|
||||
},
|
||||
setSubnetMFAToken: (state, action: PayloadAction<string>) => {
|
||||
state.subnetMFAToken = action.payload;
|
||||
},
|
||||
setSubnetOTP: (state, action: PayloadAction<string>) => {
|
||||
state.subnetOTP = action.payload;
|
||||
},
|
||||
setSubnetAccessToken: (state, action: PayloadAction<string>) => {
|
||||
state.subnetAccessToken = action.payload;
|
||||
},
|
||||
setSelectedSubnetOrganization: (state, action: PayloadAction<string>) => {
|
||||
state.selectedSubnetOrganization = action.payload;
|
||||
},
|
||||
setSubnetRegToken: (state, action: PayloadAction<string>) => {
|
||||
state.subnetRegToken = action.payload;
|
||||
},
|
||||
setSubnetOrganizations: (
|
||||
state,
|
||||
action: PayloadAction<SubnetOrganization[]>
|
||||
) => {
|
||||
state.subnetOrganizations = action.payload;
|
||||
},
|
||||
setShowPassword: (state, action: PayloadAction<boolean>) => {
|
||||
state.showPassword = action.payload;
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
},
|
||||
setLoadingLicenseInfo: (state, action: PayloadAction<boolean>) => {
|
||||
state.loadingLicenseInfo = action.payload;
|
||||
},
|
||||
setClusterRegistered: (state, action: PayloadAction<boolean>) => {
|
||||
state.clusterRegistered = action.payload;
|
||||
},
|
||||
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | undefined>) => {
|
||||
state.licenseInfo = action.payload;
|
||||
},
|
||||
setCurTab: (state, action: PayloadAction<number>) => {
|
||||
state.curTab = action.payload;
|
||||
},
|
||||
resetRegisterForm: () => initialState,
|
||||
},
|
||||
});
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const {
|
||||
setLicense,
|
||||
setSubnetPassword,
|
||||
setSubnetEmail,
|
||||
setSubnetMFAToken,
|
||||
setSubnetOTP,
|
||||
setSubnetAccessToken,
|
||||
setSelectedSubnetOrganization,
|
||||
setSubnetRegToken,
|
||||
setSubnetOrganizations,
|
||||
setShowPassword,
|
||||
setLoading,
|
||||
setLoadingLicenseInfo,
|
||||
setClusterRegistered,
|
||||
setLicenseInfo,
|
||||
setCurTab,
|
||||
resetRegisterForm,
|
||||
} = registerSlice.actions;
|
||||
|
||||
export default registerSlice.reducer;
|
||||
211
portal-ui/src/screens/Console/Support/registerThunks.ts
Normal file
211
portal-ui/src/screens/Console/Support/registerThunks.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2023 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 {
|
||||
resetRegisterForm,
|
||||
setClusterRegistered,
|
||||
setLicenseInfo,
|
||||
setLoading,
|
||||
setLoadingLicenseInfo,
|
||||
setSelectedSubnetOrganization,
|
||||
setSubnetAccessToken,
|
||||
setSubnetMFAToken,
|
||||
setSubnetOrganizations,
|
||||
setSubnetOTP,
|
||||
} from "./registerSlice";
|
||||
import api from "../../../common/api";
|
||||
import {
|
||||
SubnetInfo,
|
||||
SubnetLoginRequest,
|
||||
SubnetLoginResponse,
|
||||
SubnetLoginWithMFARequest,
|
||||
SubnetRegisterRequest,
|
||||
} from "../License/types";
|
||||
import { ErrorResponseHandler } from "../../../common/types";
|
||||
import { setErrorSnackMessage } from "../../../systemSlice";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { AppState } from "../../../store";
|
||||
import { hasPermission } from "../../../common/SecureComponent";
|
||||
import {
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES,
|
||||
IAM_PAGES_PERMISSIONS,
|
||||
} from "../../../common/SecureComponent/permissions";
|
||||
|
||||
export const fetchLicenseInfo = createAsyncThunk(
|
||||
"register/fetchLicenseInfo",
|
||||
async (_, { getState, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
const getSubnetInfo = hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
|
||||
true
|
||||
);
|
||||
|
||||
const loadingLicenseInfo = state.register.loadingLicenseInfo;
|
||||
|
||||
if (loadingLicenseInfo) {
|
||||
return;
|
||||
}
|
||||
if (getSubnetInfo) {
|
||||
dispatch(setLoadingLicenseInfo(true));
|
||||
api
|
||||
.invoke("GET", `/api/v1/subnet/info`)
|
||||
.then((res: SubnetInfo) => {
|
||||
dispatch(setLicenseInfo(res));
|
||||
dispatch(setClusterRegistered(true));
|
||||
dispatch(setLoadingLicenseInfo(false));
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
if (
|
||||
err.detailedError.toLowerCase() !==
|
||||
"License is not present".toLowerCase() &&
|
||||
err.detailedError.toLowerCase() !==
|
||||
"license not found".toLowerCase()
|
||||
) {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
}
|
||||
dispatch(setClusterRegistered(false));
|
||||
dispatch(setLoadingLicenseInfo(false));
|
||||
});
|
||||
} else {
|
||||
dispatch(setLoadingLicenseInfo(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface ClassRegisterArgs {
|
||||
token: string;
|
||||
account_id: string;
|
||||
}
|
||||
|
||||
export const callRegister = createAsyncThunk(
|
||||
"register/callRegister",
|
||||
async (args: ClassRegisterArgs, { dispatch }) => {
|
||||
const request: SubnetRegisterRequest = {
|
||||
token: args.token,
|
||||
account_id: args.account_id,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/register", request)
|
||||
.then(() => {
|
||||
dispatch(setLoading(false));
|
||||
dispatch(resetRegisterForm());
|
||||
dispatch(fetchLicenseInfo());
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
dispatch(setLoading(false));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const subnetLoginWithMFA = createAsyncThunk(
|
||||
"register/subnetLoginWithMFA",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
const subnetEmail = state.register.subnetEmail;
|
||||
const subnetMFAToken = state.register.subnetMFAToken;
|
||||
const subnetOTP = state.register.subnetOTP;
|
||||
const loading = state.register.loading;
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
dispatch(setLoading(true));
|
||||
const request: SubnetLoginWithMFARequest = {
|
||||
username: subnetEmail,
|
||||
otp: subnetOTP,
|
||||
mfa_token: subnetMFAToken,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/login/mfa", request)
|
||||
.then((resp: SubnetLoginResponse) => {
|
||||
dispatch(setLoading(false));
|
||||
if (resp && resp.access_token && resp.organizations.length > 0) {
|
||||
if (resp.organizations.length === 1) {
|
||||
dispatch(
|
||||
callRegister({
|
||||
token: resp.access_token,
|
||||
account_id: resp.organizations[0].accountId.toString(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(setSubnetAccessToken(resp.access_token));
|
||||
dispatch(setSubnetOrganizations(resp.organizations));
|
||||
dispatch(
|
||||
setSelectedSubnetOrganization(
|
||||
resp.organizations[0].accountId.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setSubnetOTP(""));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const subnetLogin = createAsyncThunk(
|
||||
"register/subnetLogin",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
const license = state.register.license;
|
||||
const subnetPassword = state.register.subnetPassword;
|
||||
const subnetEmail = state.register.subnetEmail;
|
||||
const loading = state.register.loading;
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
dispatch(setLoading(true));
|
||||
let request: SubnetLoginRequest = {
|
||||
username: subnetEmail,
|
||||
password: subnetPassword,
|
||||
apiKey: license,
|
||||
};
|
||||
api
|
||||
.invoke("POST", "/api/v1/subnet/login", request)
|
||||
.then((resp: SubnetLoginResponse) => {
|
||||
dispatch(setLoading(false));
|
||||
if (resp && resp.registered) {
|
||||
dispatch(resetRegisterForm());
|
||||
dispatch(fetchLicenseInfo());
|
||||
} else if (resp && resp.mfa_token) {
|
||||
dispatch(setSubnetMFAToken(resp.mfa_token));
|
||||
} else if (resp && resp.access_token && resp.organizations.length > 0) {
|
||||
dispatch(setSubnetAccessToken(resp.access_token));
|
||||
dispatch(setSubnetOrganizations(resp.organizations));
|
||||
dispatch(
|
||||
setSelectedSubnetOrganization(
|
||||
resp.organizations[0].accountId.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
dispatch(setLoading(false));
|
||||
dispatch(resetRegisterForm());
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -58,7 +58,7 @@ export const ClusterRegistered = ({ email }: { email: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
|
||||
export const ProxyConfiguration = () => {
|
||||
const proxyConfigurationCommand =
|
||||
"mc admin config set {alias} subnet proxy={proxy}";
|
||||
const [displaySubnetProxy, setDisplaySubnetProxy] = useState(false);
|
||||
@@ -104,7 +104,10 @@ export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
|
||||
>
|
||||
For airgap/firewalled environments it is possible to{" "}
|
||||
<Link
|
||||
className={linkClass}
|
||||
style={{
|
||||
color: "#2781B0",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
href="https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-config.html?ref=con"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -37,6 +37,7 @@ import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetai
|
||||
import editTenantSecurityContextReducer from "./screens/Console/Tenants/tenantSecurityContextSlice";
|
||||
import directPVReducer from "./screens/Console/DirectPV/directPVSlice";
|
||||
import licenseReducer from "./screens/Console/License/licenseSlice";
|
||||
import registerReducer from "./screens/Console/Support/registerSlice";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
system: systemReducer,
|
||||
@@ -50,6 +51,7 @@ const rootReducer = combineReducers({
|
||||
objectBrowser: objectBrowserReducer,
|
||||
healthInfo: healthInfoReducer,
|
||||
dashboard: dashboardReducer,
|
||||
register: registerReducer,
|
||||
// Operator Reducers
|
||||
tenants: tenantsReducer,
|
||||
createTenant: createTenantReducer,
|
||||
|
||||
@@ -62,7 +62,7 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) {
|
||||
})
|
||||
// Get subnet info
|
||||
api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := GetSubnetInfoResponse(params)
|
||||
resp, err := GetSubnetInfoResponse(session, params)
|
||||
if err != nil {
|
||||
return subnetApi.NewSubnetInfoDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
@@ -318,14 +318,47 @@ func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSubnetInfoResponse(params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
|
||||
var ErrSubnetLicenseNotFound = errors.New("license not found")
|
||||
|
||||
func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
client := &xhttp.Client{
|
||||
Client: GetConsoleHTTPClient(""),
|
||||
}
|
||||
// license gets seeded to us by MinIO
|
||||
seededLicense := os.Getenv(EnvSubnetLicense)
|
||||
// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
|
||||
if seededLicense == "" {
|
||||
mAdmin, err := NewMinioAdminClient(session)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
adminClient := AdminClient{Client: mAdmin}
|
||||
|
||||
licenseInfo, err := subnet.ParseLicense(client, os.Getenv(EnvSubnetLicense))
|
||||
configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
// search for licese
|
||||
for _, v := range subSysConfigs {
|
||||
for _, sv := range v.KV {
|
||||
if sv.Key == "license" {
|
||||
seededLicense = sv.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// still empty means not found
|
||||
if seededLicense == "" {
|
||||
return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
|
||||
}
|
||||
|
||||
licenseInfo, err := subnet.ParseLicense(client, seededLicense)
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user