UI Forms Cleanup (#3538)

This commit is contained in:
Alex
2025-05-14 20:24:04 -06:00
committed by GitHub
parent 3dc0fdc039
commit 7871f6bc27
6 changed files with 75 additions and 432 deletions

View File

@@ -23,22 +23,11 @@ import { baseUrl } from "./history";
const Login = React.lazy(() => import("./screens/LoginPage/Login")); const Login = React.lazy(() => import("./screens/LoginPage/Login"));
const Logout = React.lazy(() => import("./screens/LogoutPage/LogoutPage")); const Logout = React.lazy(() => import("./screens/LogoutPage/LogoutPage"));
const LoginCallback = React.lazy(
() => import("./screens/LoginPage/LoginCallback"),
);
const MainRouter = () => { const MainRouter = () => {
return ( return (
<BrowserRouter basename={baseUrl}> <BrowserRouter basename={baseUrl}>
<Routes> <Routes>
<Route
path="/oauth_callback"
element={
<Suspense fallback={<LoadingComponent />}>
<LoginCallback />
</Suspense>
}
/>
<Route <Route
path="/logout" path="/logout"
element={ element={

View File

@@ -53,8 +53,6 @@ const Login = () => {
); );
const navigateTo = useSelector((state: AppState) => state.login.navigateTo); const navigateTo = useSelector((state: AppState) => state.login.navigateTo);
const isK8S = useSelector((state: AppState) => state.login.isK8S);
const backgroundAnimation = useSelector( const backgroundAnimation = useSelector(
(state: AppState) => state.login.backgroundAnimation, (state: AppState) => state.login.backgroundAnimation,
); );
@@ -134,10 +132,6 @@ const Login = () => {
} }
let docsURL = "https://min.io/docs/minio/linux/index.html?ref=con"; let docsURL = "https://min.io/docs/minio/linux/index.html?ref=con";
if (isK8S) {
docsURL =
"https://min.io/docs/minio/kubernetes/upstream/index.html?ref=con";
}
useEffect(() => { useEffect(() => {
dispatch(setHelpName("login")); dispatch(setHelpName("login"));

View File

@@ -1,159 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
import api from "../../common/api";
import { baseUrl } from "../../history";
import { Box, Button, LoginWrapper, WarnIcon } from "mds";
import { getLogoApplicationVariant, getLogoVar } from "../../config";
import get from "lodash/get";
const CallBackContainer = styled.div(({ theme }) => ({
"& .errorDescription": {
fontStyle: "italic",
transition: "all .2s ease-in-out",
padding: "0 15px",
marginTop: 5,
overflow: "auto",
},
"& .errorLabel": {
color: get(theme, "fontColor", "#000"),
fontSize: 18,
fontWeight: "bold",
marginLeft: 5,
},
"& .simpleError": {
marginTop: 5,
padding: "2px 5px",
fontSize: 16,
color: get(theme, "fontColor", "#000"),
},
"& .messageIcon": {
color: get(theme, "signalColors.danger", "#C72C48"),
display: "flex",
"& svg": {
width: 32,
height: 32,
},
},
"& .errorTitle": {
display: "flex",
alignItems: "center",
borderBottom: 15,
},
}));
const LoginCallback = () => {
const navigate = useNavigate();
const [error, setError] = useState<string>("");
const [errorDescription, setErrorDescription] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (loading) {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const code = urlParams.get("code");
const state = urlParams.get("state");
const error = urlParams.get("error");
const errorDescription = urlParams.get("errorDescription");
if (error || errorDescription) {
setError(error || "");
setErrorDescription(errorDescription || "");
setLoading(false);
} else {
api
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
.then(() => {
// We push to history the new URL.
let targetPath = "/";
if (
localStorage.getItem("redirect-path") &&
localStorage.getItem("redirect-path") !== ""
) {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
if (state) {
localStorage.setItem("auth-state", state);
}
setLoading(false);
navigate(targetPath);
})
.catch((error) => {
setError(error.errorMessage);
setErrorDescription(error.detailedError);
setLoading(false);
});
}
}
}, [loading, navigate]);
return error !== "" || errorDescription !== "" ? (
<Fragment>
<LoginWrapper
logoProps={{
applicationName: getLogoApplicationVariant(),
subVariant: getLogoVar(),
}}
form={
<CallBackContainer>
<div className={"errorTitle"}>
<span className={"messageIcon"}>
<WarnIcon />
</span>
<span className={"errorLabel"}>Error from IDP</span>
</div>
<div className={"simpleError"}>{error}</div>
<Box className={"errorDescription"}>{errorDescription}</Box>
<Button
id={"back-to-login"}
onClick={() => {
window.location.href = `${baseUrl}login`;
}}
type="submit"
variant="callAction"
fullWidth
>
Back to Login
</Button>
</CallBackContainer>
}
promoHeader={
<span style={{ fontSize: 28 }}>High-Performance Object Store</span>
}
promoInfo={
<span style={{ fontSize: 14, lineHeight: 1 }}>
MinIO is a cloud-native object store built to run on any
infrastructure - public, private or edge clouds. Primary use cases
include data lakes, databases, AI/ML, SaaS applications and fast
backup & recovery. MinIO is dual licensed under GNU AGPL v3 and
commercial license. To learn more, visit{" "}
<a href={"https://min.io/?ref=con"} target="_blank" rel="noopener">
www.min.io
</a>
.
</span>
}
backgroundAnimation={false}
/>
</Fragment>
) : null;
};
export default LoginCallback;

View File

@@ -14,27 +14,16 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react"; import React, { Fragment } from "react";
import { import {
Box,
Button, Button,
DropdownSelector,
Grid, Grid,
InputBox, InputBox,
LockFilledIcon, LockFilledIcon,
LogoutIcon,
PasswordKeyIcon,
ProgressBar, ProgressBar,
Select,
UserFilledIcon, UserFilledIcon,
} from "mds"; } from "mds";
import { import { setAccessKey, setSecretKey } from "./loginSlice";
setAccessKey,
setDisplayEmbeddedIDPForms,
setSecretKey,
setSTS,
setUseSTS,
} from "./loginSlice";
import { AppState, useAppDispatch } from "../../store"; import { AppState, useAppDispatch } from "../../store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -44,18 +33,8 @@ import { RedirectRule } from "api/consoleApi";
const StrategyForm = ({ redirectRules }: { redirectRules: RedirectRule[] }) => { const StrategyForm = ({ redirectRules }: { redirectRules: RedirectRule[] }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [ssoOptionsOpen, ssoOptionsSetOpen] = useState<boolean>(false);
const [anchorEl, setAnchorEl] = React.useState<
(EventTarget & HTMLButtonElement) | null
>(null);
const accessKey = useSelector((state: AppState) => state.login.accessKey); const accessKey = useSelector((state: AppState) => state.login.accessKey);
const secretKey = useSelector((state: AppState) => state.login.secretKey); const secretKey = useSelector((state: AppState) => state.login.secretKey);
const sts = useSelector((state: AppState) => state.login.sts);
const useSTS = useSelector((state: AppState) => state.login.useSTS);
const displaySSOForm = useSelector(
(state: AppState) => state.login.ssoEmbeddedIDPDisplay,
);
const loginSending = useSelector( const loginSending = useSelector(
(state: AppState) => state.login.loginSending, (state: AppState) => state.login.loginSending,
@@ -66,210 +45,84 @@ const StrategyForm = ({ redirectRules }: { redirectRules: RedirectRule[] }) => {
dispatch(doLoginAsync()); dispatch(doLoginAsync());
}; };
let selectOptions = [
{
label: useSTS ? "Use Credentials" : "Use STS",
value: useSTS ? "use-sts-cred" : "use-sts",
},
];
let ssoOptions: any[] = [];
if (redirectRules.length > 0) {
ssoOptions = redirectRules.map((r) => ({
label: `${r.displayName}${r.serviceType ? ` - ${r.serviceType}` : ""}`,
value: r.redirect,
icon: <LogoutIcon />,
}));
selectOptions = [
{ label: "Use Credentials", value: "use-sts-cred" },
{ label: "Use STS", value: "use-sts" },
];
}
const extraActionSelector = (value: string) => {
if (value) {
if (redirectRules.length > 0) {
let stsState = true;
if (value === "use-sts-cred") {
stsState = false;
}
dispatch(setUseSTS(stsState));
dispatch(setDisplayEmbeddedIDPForms(true));
return;
}
if (value.includes("use-sts")) {
dispatch(setUseSTS(!useSTS));
return;
}
}
};
const submitSSOInitRequest = (value: string) => {
window.location.href = value;
};
return ( return (
<React.Fragment> <React.Fragment>
{redirectRules.length > 0 && (
<Fragment>
<Box sx={{ marginBottom: 40 }}>
<Button
id={"SSOSelector"}
variant={"subAction"}
label={
redirectRules.length === 1
? `${redirectRules[0].displayName}${
redirectRules[0].serviceType
? ` - ${redirectRules[0].serviceType}`
: ""
}`
: `Login with SSO`
}
fullWidth
sx={{ height: 50 }}
onClick={(e) => {
if (redirectRules.length > 1) {
ssoOptionsSetOpen(!ssoOptionsOpen);
setAnchorEl(e.currentTarget);
return;
}
submitSSOInitRequest(`${redirectRules[0].redirect}`);
}}
/>
{redirectRules.length > 1 && (
<DropdownSelector
id={"redirect-rules"}
options={ssoOptions}
selectedOption={""}
onSelect={(nValue) => submitSSOInitRequest(nValue)}
hideTriggerAction={() => {
ssoOptionsSetOpen(false);
}}
open={ssoOptionsOpen}
anchorEl={anchorEl}
useAnchorWidth={true}
/>
)}
</Box>
</Fragment>
)}
<form noValidate onSubmit={formSubmit} style={{ width: "100%" }}> <form noValidate onSubmit={formSubmit} style={{ width: "100%" }}>
{((displaySSOForm && redirectRules.length > 0) || <Fragment>
redirectRules.length === 0) && ( <Grid
<Fragment> container
<Grid sx={{
container marginTop: redirectRules.length > 0 ? 55 : 0,
sx={{ }}
marginTop: redirectRules.length > 0 ? 55 : 0, >
}} <Grid item xs={12} sx={{ marginBottom: 14 }}>
> <InputBox
<Grid item xs={12} sx={{ marginBottom: 14 }}>
<InputBox
fullWidth
id="accessKey"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setAccessKey(e.target.value))
}
placeholder={useSTS ? "STS Username" : "Username"}
name="accessKey"
autoComplete="username"
disabled={loginSending}
startIcon={<UserFilledIcon />}
/>
</Grid>
<Grid item xs={12} sx={{ marginBottom: useSTS ? 14 : 0 }}>
<InputBox
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSecretKey(e.target.value))
}
name="secretKey"
type="password"
id="secretKey"
autoComplete="current-password"
disabled={loginSending}
placeholder={useSTS ? "STS Secret" : "Password"}
startIcon={<LockFilledIcon />}
/>
</Grid>
{useSTS && (
<Grid item xs={12}>
<InputBox
fullWidth
id="sts"
value={sts}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSTS(e.target.value))
}
placeholder={"STS Token"}
name="STS"
autoComplete="sts"
disabled={loginSending}
startIcon={<PasswordKeyIcon />}
/>
</Grid>
)}
</Grid>
<Grid
item
xs={12}
sx={{
textAlign: "right",
marginTop: 30,
}}
>
<Button
type="submit"
variant="callAction"
color="primary"
id="do-login"
disabled={
(!useSTS && (accessKey === "" || secretKey === "")) ||
(useSTS &&
(accessKey === "" || secretKey === "" || sts === "")) ||
loginSending
}
label={"Login"}
sx={{
margin: "30px 0px 8px",
height: 40,
width: "100%",
boxShadow: "none",
padding: "16px 30px",
}}
fullWidth fullWidth
id="accessKey"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setAccessKey(e.target.value))
}
placeholder={"Username"}
name="accessKey"
autoComplete="username"
disabled={loginSending}
startIcon={<UserFilledIcon />}
/> />
</Grid> </Grid>
<Grid <Grid item xs={12}>
item <InputBox
xs={12} fullWidth
sx={{ value={secretKey}
height: 10, onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
}} dispatch(setSecretKey(e.target.value))
> }
{loginSending && <ProgressBar />} name="secretKey"
type="password"
id="secretKey"
autoComplete="current-password"
disabled={loginSending}
placeholder={"Password"}
startIcon={<LockFilledIcon />}
/>
</Grid> </Grid>
</Fragment> </Grid>
)}
<Grid item xs={12} sx={{ marginTop: 45 }}> <Grid
<Select item
id="alternativeMethods" xs={12}
name="alternativeMethods" sx={{
fixedLabel="Other Authentication Methods" textAlign: "right",
options={selectOptions} marginTop: 30,
onChange={extraActionSelector} }}
value={""} >
/> <Button
</Grid> type="submit"
variant="callAction"
color="primary"
id="do-login"
disabled={accessKey === "" || secretKey === "" || loginSending}
label={"Login"}
sx={{
margin: "30px 0px 8px",
height: 40,
width: "100%",
boxShadow: "none",
padding: "16px 30px",
}}
fullWidth
/>
</Grid>
<Grid
item
xs={12}
sx={{
height: 10,
}}
>
{loginSending && <ProgressBar />}
</Grid>
</Fragment>
</form> </form>
</React.Fragment> </React.Fragment>
); );

View File

@@ -21,32 +21,24 @@ import { doLoginAsync, getFetchConfigurationAsync } from "./loginThunks";
interface LoginState { interface LoginState {
accessKey: string; accessKey: string;
secretKey: string; secretKey: string;
sts: string;
useSTS: boolean;
backgroundAnimation: boolean; backgroundAnimation: boolean;
loginStrategy: LoginDetails; loginStrategy: LoginDetails;
loginSending: boolean; loginSending: boolean;
loadingFetchConfiguration: boolean; loadingFetchConfiguration: boolean;
isK8S: boolean;
navigateTo: string; navigateTo: string;
ssoEmbeddedIDPDisplay: boolean;
} }
const initialState: LoginState = { const initialState: LoginState = {
accessKey: "", accessKey: "",
secretKey: "", secretKey: "",
sts: "",
useSTS: false,
loginStrategy: { loginStrategy: {
loginStrategy: undefined, loginStrategy: undefined,
redirectRules: [], redirectRules: [],
}, },
loginSending: false, loginSending: false,
loadingFetchConfiguration: true, loadingFetchConfiguration: true,
isK8S: false,
backgroundAnimation: false, backgroundAnimation: false,
navigateTo: "", navigateTo: "",
ssoEmbeddedIDPDisplay: false,
}; };
const loginSlice = createSlice({ const loginSlice = createSlice({
@@ -59,18 +51,9 @@ const loginSlice = createSlice({
setSecretKey: (state, action: PayloadAction<string>) => { setSecretKey: (state, action: PayloadAction<string>) => {
state.secretKey = action.payload; state.secretKey = action.payload;
}, },
setUseSTS: (state, action: PayloadAction<boolean>) => {
state.useSTS = action.payload;
},
setSTS: (state, action: PayloadAction<string>) => {
state.sts = action.payload;
},
setNavigateTo: (state, action: PayloadAction<string>) => { setNavigateTo: (state, action: PayloadAction<string>) => {
state.navigateTo = action.payload; state.navigateTo = action.payload;
}, },
setDisplayEmbeddedIDPForms: (state, action: PayloadAction<boolean>) => {
state.ssoEmbeddedIDPDisplay = action.payload;
},
resetForm: (state) => initialState, resetForm: (state) => initialState,
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
@@ -85,7 +68,6 @@ const loginSlice = createSlice({
state.loadingFetchConfiguration = false; state.loadingFetchConfiguration = false;
if (action.payload) { if (action.payload) {
state.loginStrategy = action.payload; state.loginStrategy = action.payload;
state.isK8S = !!action.payload.isK8S;
state.backgroundAnimation = !!action.payload.animatedLogin; state.backgroundAnimation = !!action.payload.animatedLogin;
} }
}) })
@@ -102,14 +84,7 @@ const loginSlice = createSlice({
}); });
// Action creators are generated for each case reducer function // Action creators are generated for each case reducer function
export const { export const { setAccessKey, setSecretKey, setNavigateTo, resetForm } =
setAccessKey, loginSlice.actions;
setSecretKey,
setUseSTS,
setSTS,
setNavigateTo,
setDisplayEmbeddedIDPForms,
resetForm,
} = loginSlice.actions;
export default loginSlice.reducer; export default loginSlice.reducer;

View File

@@ -34,20 +34,11 @@ export const doLoginAsync = createAsyncThunk(
const state = getState() as AppState; const state = getState() as AppState;
const accessKey = state.login.accessKey; const accessKey = state.login.accessKey;
const secretKey = state.login.secretKey; const secretKey = state.login.secretKey;
const sts = state.login.sts;
const useSTS = state.login.useSTS;
let payload: LoginRequest = { let payload: LoginRequest = {
accessKey, accessKey,
secretKey, secretKey,
}; };
if (useSTS) {
payload = {
accessKey,
secretKey,
sts,
};
}
return api.login return api.login
.login(payload) .login(payload)