Object Browser only mode (#2157)
- Added flag CONSOLE_OBJECT_BROWSER_ONLY=on to trigger between console mode & Object Browser only - Hidden not necessary buttons for object browse - STS Login Signed-off-by: Benjamin Perez <benjamin@bexsoft.net> Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -36,13 +36,15 @@ import {
|
||||
IAM_ROLES,
|
||||
IAM_SCOPES,
|
||||
} from "../../../../common/SecureComponent/permissions";
|
||||
import SearchBox from "../../Common/SearchBox";
|
||||
import BackLink from "../../../../common/BackLink";
|
||||
import {
|
||||
setSearchObjects,
|
||||
setSearchVersions,
|
||||
setVersionsModeEnabled,
|
||||
} from "../../ObjectBrowser/objectBrowserSlice";
|
||||
import SearchBox from "../../Common/SearchBox";
|
||||
import { selFeatures } from "../../consoleSlice";
|
||||
import { LoginMinIOLogo } from "../../../../icons";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -67,9 +69,13 @@ const BrowserHandler = () => {
|
||||
(state: AppState) => state.objectBrowser.searchVersions
|
||||
);
|
||||
|
||||
const features = useSelector(selFeatures);
|
||||
|
||||
const bucketName = params.bucketName || "";
|
||||
const internalPaths = get(params, "subpaths", "");
|
||||
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setVersionsModeEnabled({ status: false }));
|
||||
}, [internalPaths, dispatch]);
|
||||
@@ -78,59 +84,79 @@ const BrowserHandler = () => {
|
||||
navigate(`/buckets/${bucketName}/admin`);
|
||||
};
|
||||
|
||||
const searchBar = (
|
||||
<Fragment>
|
||||
{!versionsMode ? (
|
||||
<SecureComponent
|
||||
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
|
||||
resource={bucketName}
|
||||
errorProps={{ disabled: true }}
|
||||
>
|
||||
<SearchBox
|
||||
placeholder={"Start typing to filter objects in the bucket"}
|
||||
onChange={(value) => {
|
||||
dispatch(setSearchObjects(value));
|
||||
}}
|
||||
value={searchObjects}
|
||||
/>
|
||||
</SecureComponent>
|
||||
) : (
|
||||
<Fragment>
|
||||
<SearchBox
|
||||
placeholder={`Start typing to filter versions of ${versionedFile}`}
|
||||
onChange={(value) => {
|
||||
dispatch(setSearchVersions(value));
|
||||
}}
|
||||
value={searchVersions}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageHeader
|
||||
label={<BackLink label={"Buckets"} to={IAM_PAGES.BUCKETS} />}
|
||||
actions={
|
||||
<SecureComponent
|
||||
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
||||
resource={bucketName}
|
||||
errorProps={{ disabled: true }}
|
||||
>
|
||||
<Tooltip title={"Configure Bucket"}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Configure Bucket"
|
||||
component="span"
|
||||
onClick={openBucketConfiguration}
|
||||
size="large"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</SecureComponent>
|
||||
}
|
||||
middleComponent={
|
||||
<Fragment>
|
||||
{!versionsMode ? (
|
||||
<SecureComponent
|
||||
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
|
||||
resource={bucketName}
|
||||
errorProps={{ disabled: true }}
|
||||
>
|
||||
<SearchBox
|
||||
placeholder={"Start typing to filter objects in the bucket"}
|
||||
onChange={(value) => {
|
||||
dispatch(setSearchObjects(value));
|
||||
}}
|
||||
value={searchObjects}
|
||||
/>
|
||||
</SecureComponent>
|
||||
) : (
|
||||
<Fragment>
|
||||
<SearchBox
|
||||
placeholder={`Start typing to filter versions of ${versionedFile}`}
|
||||
onChange={(value) => {
|
||||
dispatch(setSearchVersions(value));
|
||||
}}
|
||||
value={searchVersions}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
{!obOnly ? (
|
||||
<PageHeader
|
||||
label={<BackLink label={"Buckets"} to={IAM_PAGES.BUCKETS} />}
|
||||
actions={
|
||||
<SecureComponent
|
||||
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
||||
resource={bucketName}
|
||||
errorProps={{ disabled: true }}
|
||||
>
|
||||
<Tooltip title={"Configure Bucket"}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Configure Bucket"
|
||||
component="span"
|
||||
onClick={openBucketConfiguration}
|
||||
size="large"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</SecureComponent>
|
||||
}
|
||||
middleComponent={searchBar}
|
||||
/>
|
||||
) : (
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
padding: "20px 32px 0",
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<LoginMinIOLogo
|
||||
style={{ width: 105, marginRight: 30, marginTop: 10 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
{searchBar}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid>
|
||||
<ListObjects />
|
||||
</Grid>
|
||||
|
||||
@@ -163,6 +163,7 @@ interface IBucketListItem {
|
||||
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
selected: boolean;
|
||||
bulkSelect: boolean;
|
||||
noManage?: boolean;
|
||||
}
|
||||
|
||||
const BucketListItem = ({
|
||||
@@ -171,6 +172,7 @@ const BucketListItem = ({
|
||||
onSelect,
|
||||
selected,
|
||||
bulkSelect,
|
||||
noManage = false,
|
||||
}: IBucketListItem) => {
|
||||
const usage = niceBytes(`${bucket.size}` || "0");
|
||||
const usageScalar = usage.split(" ")[0];
|
||||
@@ -236,24 +238,26 @@ const BucketListItem = ({
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={5} className={classes.bucketActionButtons}>
|
||||
<SecureComponent
|
||||
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
||||
resource={bucket.name}
|
||||
>
|
||||
<Link
|
||||
to={`/buckets/${bucket.name}/admin`}
|
||||
style={{ textDecoration: "none" }}
|
||||
{!noManage && (
|
||||
<SecureComponent
|
||||
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
||||
resource={bucket.name}
|
||||
>
|
||||
<RBIconButton
|
||||
tooltip={"Manage"}
|
||||
onClick={() => {}}
|
||||
text={"Manage"}
|
||||
icon={<SettingsIcon />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
</Link>
|
||||
</SecureComponent>
|
||||
<Link
|
||||
to={`/buckets/${bucket.name}/admin`}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<RBIconButton
|
||||
tooltip={"Manage"}
|
||||
onClick={() => {}}
|
||||
text={"Manage"}
|
||||
icon={<SettingsIcon />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
</Link>
|
||||
</SecureComponent>
|
||||
)}
|
||||
<Link
|
||||
to={`/buckets/${bucket.name}/browse`}
|
||||
style={{ textDecoration: "none" }}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
AddIcon,
|
||||
BucketsIcon,
|
||||
LifecycleConfigIcon,
|
||||
LoginMinIOLogo,
|
||||
SelectAllIcon,
|
||||
} from "../../../../icons";
|
||||
import {
|
||||
@@ -57,6 +58,8 @@ import BulkLifecycleModal from "./BulkLifecycleModal";
|
||||
import hasPermission from "../../../../common/SecureComponent/accessControl";
|
||||
import { setErrorSnackMessage } from "../../../../systemSlice";
|
||||
import { useAppDispatch } from "../../../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selFeatures } from "../../consoleSlice";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -98,9 +101,11 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
const [replicationModalOpen, setReplicationModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const [lifecycleModalOpen, setLifecycleModalOpen] = useState<boolean>(false);
|
||||
|
||||
const [bulkSelect, setBulkSelect] = useState<boolean>(false);
|
||||
|
||||
const features = useSelector(selFeatures);
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
const fetchRecords = () => {
|
||||
@@ -172,6 +177,7 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
onSelect={selectListBuckets}
|
||||
selected={selectedBuckets.includes(bucket.name)}
|
||||
bulkSelect={bulkSelect}
|
||||
noManage={obOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -209,9 +215,16 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
open={lifecycleModalOpen}
|
||||
/>
|
||||
)}
|
||||
<PageHeader label={"Buckets"} />
|
||||
{!obOnly && <PageHeader label={"Buckets"} />}
|
||||
<PageLayout>
|
||||
<Grid item xs={12} className={classes.actionsTray} display="flex">
|
||||
{obOnly && (
|
||||
<Grid item xs>
|
||||
<LoginMinIOLogo
|
||||
style={{ width: 105, marginRight: 15, marginTop: 10 }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<SearchBox
|
||||
onChange={setFilterBuckets}
|
||||
placeholder="Search Buckets"
|
||||
@@ -226,59 +239,63 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
alignItems={"center"}
|
||||
justifyContent={"flex-end"}
|
||||
>
|
||||
<RBIconButton
|
||||
tooltip={
|
||||
bulkSelect ? "Unselect Buckets" : "Select Multiple Buckets"
|
||||
}
|
||||
onClick={() => {
|
||||
setBulkSelect(!bulkSelect);
|
||||
setSelectedBuckets([]);
|
||||
}}
|
||||
text={""}
|
||||
icon={<SelectMultipleIcon />}
|
||||
color={"primary"}
|
||||
variant={bulkSelect ? "contained" : "outlined"}
|
||||
/>
|
||||
{!obOnly && (
|
||||
<Fragment>
|
||||
<RBIconButton
|
||||
tooltip={
|
||||
bulkSelect ? "Unselect Buckets" : "Select Multiple Buckets"
|
||||
}
|
||||
onClick={() => {
|
||||
setBulkSelect(!bulkSelect);
|
||||
setSelectedBuckets([]);
|
||||
}}
|
||||
text={""}
|
||||
icon={<SelectMultipleIcon />}
|
||||
color={"primary"}
|
||||
variant={bulkSelect ? "contained" : "outlined"}
|
||||
/>
|
||||
|
||||
{bulkSelect && (
|
||||
<RBIconButton
|
||||
tooltip={
|
||||
selectedBuckets.length === filteredRecords.length
|
||||
? "Unselect All Buckets"
|
||||
: "Select All Buckets"
|
||||
}
|
||||
onClick={selectAllBuckets}
|
||||
text={""}
|
||||
icon={<SelectAllIcon />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
{bulkSelect && (
|
||||
<RBIconButton
|
||||
tooltip={
|
||||
selectedBuckets.length === filteredRecords.length
|
||||
? "Unselect All Buckets"
|
||||
: "Select All Buckets"
|
||||
}
|
||||
onClick={selectAllBuckets}
|
||||
text={""}
|
||||
icon={<SelectAllIcon />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Set Lifecycle"}
|
||||
onClick={() => {
|
||||
setLifecycleModalOpen(true);
|
||||
}}
|
||||
text={""}
|
||||
icon={<LifecycleConfigIcon />}
|
||||
disabled={selectedBuckets.length === 0}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Set Replication"}
|
||||
onClick={() => {
|
||||
setReplicationModalOpen(true);
|
||||
}}
|
||||
text={""}
|
||||
icon={<MultipleBucketsIcon />}
|
||||
disabled={selectedBuckets.length === 0}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Set Lifecycle"}
|
||||
onClick={() => {
|
||||
setLifecycleModalOpen(true);
|
||||
}}
|
||||
text={""}
|
||||
icon={<LifecycleConfigIcon />}
|
||||
disabled={selectedBuckets.length === 0}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Set Replication"}
|
||||
onClick={() => {
|
||||
setReplicationModalOpen(true);
|
||||
}}
|
||||
text={""}
|
||||
icon={<MultipleBucketsIcon />}
|
||||
disabled={selectedBuckets.length === 0}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Refresh"}
|
||||
onClick={() => {
|
||||
@@ -290,17 +307,19 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
variant={"outlined"}
|
||||
/>
|
||||
|
||||
<RBIconButton
|
||||
tooltip={"Create Bucket"}
|
||||
onClick={() => {
|
||||
navigate(IAM_PAGES.ADD_BUCKETS);
|
||||
}}
|
||||
text={"Create Bucket"}
|
||||
icon={<AddIcon />}
|
||||
color={"primary"}
|
||||
variant={"contained"}
|
||||
disabled={!canCreateBucket}
|
||||
/>
|
||||
{!obOnly && (
|
||||
<RBIconButton
|
||||
tooltip={"Create Bucket"}
|
||||
onClick={() => {
|
||||
navigate(IAM_PAGES.ADD_BUCKETS);
|
||||
}}
|
||||
text={"Create Bucket"}
|
||||
icon={<AddIcon />}
|
||||
color={"primary"}
|
||||
variant={"contained"}
|
||||
disabled={!canCreateBucket}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -203,6 +203,8 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
|
||||
|
||||
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
|
||||
const restartServer = () => {
|
||||
dispatch(serverIsLoading(true));
|
||||
api
|
||||
@@ -461,16 +463,17 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
|
||||
const allowedRoutes = (
|
||||
operatorMode ? operatorConsoleRoutes : consoleAdminRoutes
|
||||
).filter(
|
||||
(route: any) =>
|
||||
(route.forceDisplay ||
|
||||
(route.customPermissionFnc
|
||||
? route.customPermissionFnc()
|
||||
: hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[route.path]
|
||||
))) &&
|
||||
!route.fsHidden
|
||||
).filter((route: any) =>
|
||||
obOnly
|
||||
? route.path.includes("buckets")
|
||||
: (route.forceDisplay ||
|
||||
(route.customPermissionFnc
|
||||
? route.customPermissionFnc()
|
||||
: hasPermission(
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_PAGES_PERMISSIONS[route.path]
|
||||
))) &&
|
||||
!route.fsHidden
|
||||
);
|
||||
|
||||
const closeSnackBar = () => {
|
||||
@@ -494,6 +497,8 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
hideMenu = true;
|
||||
} else if (pathname.endsWith("/hop")) {
|
||||
hideMenu = true;
|
||||
} else if (obOnly) {
|
||||
hideMenu = true;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
68
portal-ui/src/screens/LoginPage/LoginField.tsx
Normal file
68
portal-ui/src/screens/LoginPage/LoginField.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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 makeStyles from "@mui/styles/makeStyles";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { TextFieldProps } from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React from "react";
|
||||
|
||||
const inputStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
"& .MuiOutlinedInput-root": {
|
||||
paddingLeft: 0,
|
||||
"& svg": {
|
||||
marginLeft: 4,
|
||||
height: 14,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
"& input": {
|
||||
padding: 10,
|
||||
fontSize: 14,
|
||||
paddingLeft: 0,
|
||||
"&::placeholder": {
|
||||
fontSize: 12,
|
||||
},
|
||||
"@media (max-width: 900px)": {
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
"& fieldset": {},
|
||||
|
||||
"& fieldset:hover": {
|
||||
borderBottom: "2px solid #000000",
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const LoginField = (props: TextFieldProps) => {
|
||||
const classes = inputStyles();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
classes={{
|
||||
root: classes.root,
|
||||
}}
|
||||
variant="standard"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -14,25 +14,16 @@
|
||||
// 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, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
InputAdornment,
|
||||
LinearProgress,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import { Box, InputAdornment, LinearProgress } from "@mui/material";
|
||||
import { Theme, useTheme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { ILoginDetails, loginStrategyType } from "./types";
|
||||
import { ErrorResponseHandler } from "../../common/types";
|
||||
import api from "../../common/api";
|
||||
import { loginStrategyType } from "./types";
|
||||
import RefreshIcon from "../../icons/RefreshIcon";
|
||||
import MainError from "../Console/Common/MainError/MainError";
|
||||
import {
|
||||
@@ -45,20 +36,22 @@ import {
|
||||
} from "../../icons";
|
||||
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import LockFilledIcon from "../../icons/LockFilledIcon";
|
||||
import UserFilledIcon from "../../icons/UsersFilledIcon";
|
||||
import { SupportMenuIcon } from "../../icons/SidebarMenus";
|
||||
import GithubIcon from "../../icons/GithubIcon";
|
||||
import clsx from "clsx";
|
||||
import Loader from "../Console/Common/Loader/Loader";
|
||||
import { AppState, useAppDispatch } from "../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
setErrorSnackMessage,
|
||||
userLogged,
|
||||
showMarketplace,
|
||||
} from "../../systemSlice";
|
||||
import { useAppDispatch } from "../../store";
|
||||
doLoginAsync,
|
||||
getFetchConfigurationAsync,
|
||||
getVersionAsync,
|
||||
} from "./loginThunks";
|
||||
import { resetForm, setJwt } from "./loginSlice";
|
||||
import StrategyForm from "./StrategyForm";
|
||||
import { LoginField } from "./LoginField";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
position: "absolute",
|
||||
@@ -78,18 +71,6 @@ const styles = (theme: Theme) =>
|
||||
boxShadow: "none",
|
||||
padding: "16px 30px",
|
||||
},
|
||||
learnMore: {
|
||||
textAlign: "center",
|
||||
fontSize: 10,
|
||||
"& a": {
|
||||
color: "#2781B0",
|
||||
},
|
||||
"& .min-icon": {
|
||||
marginLeft: 12,
|
||||
marginTop: 2,
|
||||
width: 10,
|
||||
},
|
||||
},
|
||||
separator: {
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
@@ -227,298 +208,89 @@ const styles = (theme: Theme) =>
|
||||
},
|
||||
},
|
||||
...spacingUtils,
|
||||
});
|
||||
|
||||
const inputStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
"& .MuiOutlinedInput-root": {
|
||||
paddingLeft: 0,
|
||||
"& svg": {
|
||||
marginLeft: 4,
|
||||
height: 14,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
"& input": {
|
||||
padding: 10,
|
||||
fontSize: 14,
|
||||
paddingLeft: 0,
|
||||
"&::placeholder": {
|
||||
fontSize: 12,
|
||||
},
|
||||
"@media (max-width: 900px)": {
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
"& fieldset": {},
|
||||
|
||||
"& fieldset:hover": {
|
||||
borderBottom: "2px solid #000000",
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
function LoginField(props: TextFieldProps) {
|
||||
const classes = inputStyles();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
classes={{
|
||||
root: classes.root,
|
||||
}}
|
||||
variant="standard"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// The inferred type will look like:
|
||||
// {isOn: boolean, toggleOn: () => void}
|
||||
|
||||
interface ILoginProps {
|
||||
classes: any;
|
||||
}
|
||||
|
||||
interface LoginStrategyRoutes {
|
||||
export interface LoginStrategyRoutes {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface LoginStrategyPayload {
|
||||
export interface LoginStrategyPayload {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const Login = ({ classes }: ILoginProps) => {
|
||||
export const loginStrategyEndpoints: LoginStrategyRoutes = {
|
||||
form: "/api/v1/login",
|
||||
"service-account": "/api/v1/login/operator",
|
||||
};
|
||||
|
||||
export const getTargetPath = () => {
|
||||
let targetPath = "/";
|
||||
if (
|
||||
localStorage.getItem("redirect-path") &&
|
||||
localStorage.getItem("redirect-path") !== ""
|
||||
) {
|
||||
targetPath = `${localStorage.getItem("redirect-path")}`;
|
||||
localStorage.setItem("redirect-path", "");
|
||||
}
|
||||
return targetPath;
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
|
||||
const [accessKey, setAccessKey] = useState<string>("");
|
||||
const [jwt, setJwt] = useState<string>("");
|
||||
const [secretKey, setSecretKey] = useState<string>("");
|
||||
const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({
|
||||
loginStrategy: loginStrategyType.unknown,
|
||||
redirect: "",
|
||||
});
|
||||
const [loginSending, setLoginSending] = useState<boolean>(false);
|
||||
const [loadingFetchConfiguration, setLoadingFetchConfiguration] =
|
||||
useState<boolean>(true);
|
||||
const jwt = useSelector((state: AppState) => state.login.jwt);
|
||||
const loginStrategy = useSelector(
|
||||
(state: AppState) => state.login.loginStrategy
|
||||
);
|
||||
const loginSending = useSelector(
|
||||
(state: AppState) => state.login.loginSending
|
||||
);
|
||||
const loadingFetchConfiguration = useSelector(
|
||||
(state: AppState) => state.login.loadingFetchConfiguration
|
||||
);
|
||||
|
||||
const [latestMinIOVersion, setLatestMinIOVersion] = useState<string>("");
|
||||
const [loadingVersion, setLoadingVersion] = useState<boolean>(true);
|
||||
const latestMinIOVersion = useSelector(
|
||||
(state: AppState) => state.login.latestMinIOVersion
|
||||
);
|
||||
const loadingVersion = useSelector(
|
||||
(state: AppState) => state.login.loadingVersion
|
||||
);
|
||||
const navigateTo = useSelector((state: AppState) => state.login.navigateTo);
|
||||
|
||||
const isOperator =
|
||||
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
|
||||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
|
||||
|
||||
const loginStrategyEndpoints: LoginStrategyRoutes = {
|
||||
form: "/api/v1/login",
|
||||
"service-account": "/api/v1/login/operator",
|
||||
};
|
||||
const loginStrategyPayload: LoginStrategyPayload = {
|
||||
form: { accessKey, secretKey },
|
||||
"service-account": { jwt },
|
||||
};
|
||||
|
||||
const fetchConfiguration = () => {
|
||||
setLoadingFetchConfiguration(true);
|
||||
};
|
||||
|
||||
const getTargetPath = () => {
|
||||
let targetPath = "/";
|
||||
if (
|
||||
localStorage.getItem("redirect-path") &&
|
||||
localStorage.getItem("redirect-path") !== ""
|
||||
) {
|
||||
targetPath = `${localStorage.getItem("redirect-path")}`;
|
||||
localStorage.setItem("redirect-path", "");
|
||||
}
|
||||
return targetPath;
|
||||
};
|
||||
|
||||
const redirectAfterLogin = () => {
|
||||
navigate(getTargetPath());
|
||||
};
|
||||
|
||||
const redirectToMarketplace = () => {
|
||||
api
|
||||
.invoke("GET", "/api/v1/mp-integration/")
|
||||
.then((res: any) => {
|
||||
redirectAfterLogin(); // Email already set, continue with normal flow
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
if (err.statusCode === 404) {
|
||||
dispatch(showMarketplace(true));
|
||||
navigate("/marketplace");
|
||||
} else {
|
||||
// Unexpected error, continue with normal flow
|
||||
redirectAfterLogin();
|
||||
}
|
||||
});
|
||||
};
|
||||
if (navigateTo !== "") {
|
||||
navigate(navigateTo);
|
||||
dispatch(resetForm());
|
||||
}
|
||||
|
||||
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoginSending(true);
|
||||
api
|
||||
.invoke(
|
||||
"POST",
|
||||
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
|
||||
loginStrategyPayload[loginStrategy.loginStrategy]
|
||||
)
|
||||
.then(() => {
|
||||
// We set the state in redux
|
||||
dispatch(userLogged(true));
|
||||
if (loginStrategy.loginStrategy === loginStrategyType.form) {
|
||||
localStorage.setItem("userLoggedIn", accessKey);
|
||||
}
|
||||
if (isOperator) {
|
||||
redirectToMarketplace();
|
||||
} else {
|
||||
redirectAfterLogin();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoginSending(false);
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
dispatch(doLoginAsync());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingFetchConfiguration) {
|
||||
api
|
||||
.invoke("GET", "/api/v1/login")
|
||||
.then((loginDetails: ILoginDetails) => {
|
||||
setLoginStrategy(loginDetails);
|
||||
setLoadingFetchConfiguration(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
setLoadingFetchConfiguration(false);
|
||||
});
|
||||
dispatch(getFetchConfigurationAsync());
|
||||
}
|
||||
}, [loadingFetchConfiguration, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingVersion) {
|
||||
api
|
||||
.invoke("GET", "/api/v1/check-version")
|
||||
.then(
|
||||
({
|
||||
current_version,
|
||||
latest_version,
|
||||
}: {
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
}) => {
|
||||
setLatestMinIOVersion(latest_version);
|
||||
setLoadingVersion(false);
|
||||
}
|
||||
)
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
// try the operator version
|
||||
api
|
||||
.invoke("GET", "/api/v1/check-operator-version")
|
||||
.then(
|
||||
({
|
||||
current_version,
|
||||
latest_version,
|
||||
}: {
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
}) => {
|
||||
setLatestMinIOVersion(latest_version);
|
||||
setLoadingVersion(false);
|
||||
}
|
||||
)
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
setLoadingVersion(false);
|
||||
});
|
||||
});
|
||||
dispatch(getVersionAsync());
|
||||
}
|
||||
}, [loadingVersion, setLoadingVersion, setLatestMinIOVersion]);
|
||||
}, [dispatch, loadingVersion]);
|
||||
|
||||
let loginComponent = null;
|
||||
let loginComponent;
|
||||
|
||||
switch (loginStrategy.loginStrategy) {
|
||||
case loginStrategyType.form: {
|
||||
loginComponent = (
|
||||
<React.Fragment>
|
||||
<form className={classes.form} noValidate onSubmit={formSubmit}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} className={classes.spacerBottom}>
|
||||
<LoginField
|
||||
fullWidth
|
||||
id="accessKey"
|
||||
className={classes.inputField}
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setAccessKey(e.target.value)
|
||||
}
|
||||
placeholder={"Username"}
|
||||
name="accessKey"
|
||||
autoComplete="username"
|
||||
disabled={loginSending}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
className={classes.iconColor}
|
||||
>
|
||||
<UserFilledIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<LoginField
|
||||
fullWidth
|
||||
className={classes.inputField}
|
||||
value={secretKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSecretKey(e.target.value)
|
||||
}
|
||||
name="secretKey"
|
||||
type="password"
|
||||
id="secretKey"
|
||||
autoComplete="current-password"
|
||||
disabled={loginSending}
|
||||
placeholder={"Password"}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
className={classes.iconColor}
|
||||
>
|
||||
<LockFilledIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.submitContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
id="do-login"
|
||||
className={classes.submit}
|
||||
disabled={secretKey === "" || accessKey === "" || loginSending}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.linearPredef}>
|
||||
{loginSending && <LinearProgress />}
|
||||
</Grid>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
);
|
||||
loginComponent = <StrategyForm />;
|
||||
break;
|
||||
}
|
||||
case loginStrategyType.redirect:
|
||||
@@ -553,7 +325,7 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
id="jwt"
|
||||
value={jwt}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setJwt(e.target.value)
|
||||
dispatch(setJwt(e.target.value))
|
||||
}
|
||||
name="jwt"
|
||||
autoComplete="off"
|
||||
@@ -607,7 +379,7 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
fetchConfiguration();
|
||||
dispatch(getFetchConfigurationAsync());
|
||||
}}
|
||||
endIcon={<RefreshIcon />}
|
||||
color={"primary"}
|
||||
@@ -771,4 +543,4 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Login);
|
||||
export default Login;
|
||||
|
||||
231
portal-ui/src/screens/LoginPage/StrategyForm.tsx
Normal file
231
portal-ui/src/screens/LoginPage/StrategyForm.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
// 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 Grid from "@mui/material/Grid";
|
||||
import React, { Fragment } from "react";
|
||||
import { setAccessKey, setSecretKey, setSTS, setUseSTS } from "./loginSlice";
|
||||
import { Box, InputAdornment, LinearProgress } from "@mui/material";
|
||||
import UserFilledIcon from "../../icons/UsersFilledIcon";
|
||||
import LockFilledIcon from "../../icons/LockFilledIcon";
|
||||
import Button from "@mui/material/Button";
|
||||
import { AppState, useAppDispatch } from "../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { LoginField } from "./LoginField";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { Theme, useTheme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
|
||||
import { doLoginAsync } from "./loginThunks";
|
||||
import { PasswordKeyIcon } from "../../icons";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
},
|
||||
form: {
|
||||
width: "100%", // Fix IE 11 issue.
|
||||
},
|
||||
submit: {
|
||||
margin: "30px 0px 8px",
|
||||
height: 40,
|
||||
width: "100%",
|
||||
boxShadow: "none",
|
||||
padding: "16px 30px",
|
||||
},
|
||||
submitContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
linearPredef: {
|
||||
height: 10,
|
||||
},
|
||||
...spacingUtils,
|
||||
})
|
||||
);
|
||||
|
||||
const StrategyForm = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const accessKey = useSelector((state: AppState) => state.login.accessKey);
|
||||
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 loginSending = useSelector(
|
||||
(state: AppState) => state.login.loginSending
|
||||
);
|
||||
|
||||
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
dispatch(doLoginAsync());
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<form className={classes.form} noValidate onSubmit={formSubmit}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} className={classes.spacerBottom}>
|
||||
<LoginField
|
||||
fullWidth
|
||||
id="accessKey"
|
||||
className={classes.inputField}
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setAccessKey(e.target.value))
|
||||
}
|
||||
placeholder={useSTS ? "STS Username" : "Username"}
|
||||
name="accessKey"
|
||||
autoComplete="username"
|
||||
disabled={loginSending}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
className={classes.iconColor}
|
||||
>
|
||||
<UserFilledIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={useSTS ? classes.spacerBottom : ""}>
|
||||
<LoginField
|
||||
fullWidth
|
||||
className={classes.inputField}
|
||||
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"}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
className={classes.iconColor}
|
||||
>
|
||||
<LockFilledIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{useSTS && (
|
||||
<Grid item xs={12} className={classes.spacerBottom}>
|
||||
<LoginField
|
||||
fullWidth
|
||||
id="sts"
|
||||
className={classes.inputField}
|
||||
value={sts}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSTS(e.target.value))
|
||||
}
|
||||
placeholder={"STS Token"}
|
||||
name="STS"
|
||||
autoComplete="sts"
|
||||
disabled={loginSending}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
className={classes.iconColor}
|
||||
>
|
||||
<PasswordKeyIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} className={classes.submitContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
id="do-login"
|
||||
className={classes.submit}
|
||||
disabled={
|
||||
(!useSTS && (accessKey === "" || secretKey === "")) ||
|
||||
(useSTS && sts === "") ||
|
||||
loginSending
|
||||
}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.linearPredef}>
|
||||
{loginSending && <LinearProgress />}
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.linearPredef}>
|
||||
<Box
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
dispatch(setUseSTS(!useSTS));
|
||||
}}
|
||||
style={{
|
||||
color: theme.colors.link,
|
||||
font: "normal normal normal 12px/15px Lato",
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{!useSTS && <Fragment>Use STS</Fragment>}
|
||||
{useSTS && <Fragment>Use Credentials</Fragment>}
|
||||
</span>
|
||||
<span
|
||||
onClick={() => {
|
||||
dispatch(setUseSTS(!useSTS));
|
||||
}}
|
||||
style={{
|
||||
color: theme.colors.link,
|
||||
font: "normal normal normal 12px/15px Lato",
|
||||
textDecoration: "none",
|
||||
fontWeight: "bold",
|
||||
paddingLeft: 4,
|
||||
}}
|
||||
>
|
||||
➔
|
||||
</span>
|
||||
</Box>
|
||||
</Grid>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyForm;
|
||||
135
portal-ui/src/screens/LoginPage/loginSlice.ts
Normal file
135
portal-ui/src/screens/LoginPage/loginSlice.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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";
|
||||
import { ILoginDetails, loginStrategyType } from "./types";
|
||||
import {
|
||||
doLoginAsync,
|
||||
getFetchConfigurationAsync,
|
||||
getVersionAsync,
|
||||
} from "./loginThunks";
|
||||
|
||||
export interface LoginState {
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
sts: string;
|
||||
useSTS: boolean;
|
||||
|
||||
jwt: string;
|
||||
|
||||
loginStrategy: ILoginDetails;
|
||||
|
||||
loginSending: boolean;
|
||||
loadingFetchConfiguration: boolean;
|
||||
|
||||
latestMinIOVersion: string;
|
||||
loadingVersion: boolean;
|
||||
|
||||
navigateTo: string;
|
||||
}
|
||||
|
||||
const initialState: LoginState = {
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
sts: "",
|
||||
useSTS: false,
|
||||
jwt: "",
|
||||
loginStrategy: {
|
||||
loginStrategy: loginStrategyType.unknown,
|
||||
redirect: "",
|
||||
},
|
||||
loginSending: false,
|
||||
loadingFetchConfiguration: true,
|
||||
latestMinIOVersion: "",
|
||||
loadingVersion: true,
|
||||
|
||||
navigateTo: "",
|
||||
};
|
||||
|
||||
export const loginSlice = createSlice({
|
||||
name: "login",
|
||||
initialState,
|
||||
reducers: {
|
||||
setAccessKey: (state, action: PayloadAction<string>) => {
|
||||
state.accessKey = action.payload;
|
||||
},
|
||||
setSecretKey: (state, action: PayloadAction<string>) => {
|
||||
state.secretKey = action.payload;
|
||||
},
|
||||
setUseSTS: (state, action: PayloadAction<boolean>) => {
|
||||
state.useSTS = action.payload;
|
||||
},
|
||||
setSTS: (state, action: PayloadAction<string>) => {
|
||||
state.sts = action.payload;
|
||||
},
|
||||
setJwt: (state, action: PayloadAction<string>) => {
|
||||
state.jwt = action.payload;
|
||||
},
|
||||
setNavigateTo: (state, action: PayloadAction<string>) => {
|
||||
state.navigateTo = action.payload;
|
||||
},
|
||||
resetForm: (state) => initialState,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(getVersionAsync.pending, (state, action) => {
|
||||
state.loadingVersion = true;
|
||||
})
|
||||
.addCase(getVersionAsync.rejected, (state, action) => {
|
||||
state.loadingVersion = false;
|
||||
})
|
||||
.addCase(getVersionAsync.fulfilled, (state, action) => {
|
||||
state.loadingVersion = false;
|
||||
if (action.payload) {
|
||||
state.latestMinIOVersion = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(getFetchConfigurationAsync.pending, (state, action) => {
|
||||
state.loadingFetchConfiguration = true;
|
||||
})
|
||||
.addCase(getFetchConfigurationAsync.rejected, (state, action) => {
|
||||
state.loadingFetchConfiguration = false;
|
||||
})
|
||||
.addCase(getFetchConfigurationAsync.fulfilled, (state, action) => {
|
||||
state.loadingFetchConfiguration = false;
|
||||
if (action.payload) {
|
||||
state.loginStrategy = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(doLoginAsync.pending, (state, action) => {
|
||||
state.loginSending = true;
|
||||
})
|
||||
.addCase(doLoginAsync.rejected, (state, action) => {
|
||||
state.loginSending = false;
|
||||
})
|
||||
.addCase(doLoginAsync.fulfilled, (state, action) => {
|
||||
state.loginSending = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const {
|
||||
setAccessKey,
|
||||
setSecretKey,
|
||||
setUseSTS,
|
||||
setSTS,
|
||||
setJwt,
|
||||
setNavigateTo,
|
||||
resetForm,
|
||||
} = loginSlice.actions;
|
||||
|
||||
export default loginSlice.reducer;
|
||||
145
portal-ui/src/screens/LoginPage/loginThunks.ts
Normal file
145
portal-ui/src/screens/LoginPage/loginThunks.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { AppState } from "../../store";
|
||||
import api from "../../common/api";
|
||||
import { ErrorResponseHandler } from "../../common/types";
|
||||
import {
|
||||
setErrorSnackMessage,
|
||||
showMarketplace,
|
||||
userLogged,
|
||||
} from "../../systemSlice";
|
||||
import { ILoginDetails, loginStrategyType } from "./types";
|
||||
import { setNavigateTo } from "./loginSlice";
|
||||
import {
|
||||
getTargetPath,
|
||||
loginStrategyEndpoints,
|
||||
LoginStrategyPayload,
|
||||
} from "./LoginPage";
|
||||
|
||||
export const doLoginAsync = createAsyncThunk(
|
||||
"login/doLoginAsync",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
const loginStrategy = state.login.loginStrategy;
|
||||
const accessKey = state.login.accessKey;
|
||||
const secretKey = state.login.secretKey;
|
||||
const jwt = state.login.jwt;
|
||||
const sts = state.login.sts;
|
||||
const useSTS = state.login.useSTS;
|
||||
|
||||
const isOperator =
|
||||
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
|
||||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
|
||||
|
||||
let loginStrategyPayload: LoginStrategyPayload = {
|
||||
form: { accessKey, secretKey },
|
||||
"service-account": { jwt },
|
||||
};
|
||||
if (useSTS) {
|
||||
loginStrategyPayload = {
|
||||
form: { accessKey, secretKey, sts },
|
||||
};
|
||||
}
|
||||
|
||||
return api
|
||||
.invoke(
|
||||
"POST",
|
||||
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
|
||||
loginStrategyPayload[loginStrategy.loginStrategy]
|
||||
)
|
||||
.then(() => {
|
||||
// We set the state in redux
|
||||
dispatch(userLogged(true));
|
||||
if (loginStrategy.loginStrategy === loginStrategyType.form) {
|
||||
localStorage.setItem("userLoggedIn", accessKey);
|
||||
}
|
||||
if (isOperator) {
|
||||
api
|
||||
.invoke("GET", "/api/v1/mp-integration/")
|
||||
.then((res: any) => {
|
||||
dispatch(setNavigateTo(getTargetPath())); // Email already set, continue with normal flow
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
if (err.statusCode === 404) {
|
||||
dispatch(showMarketplace(true));
|
||||
dispatch(setNavigateTo("/marketplace"));
|
||||
} else {
|
||||
// Unexpected error, continue with normal flow
|
||||
dispatch(setNavigateTo(getTargetPath()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dispatch(setNavigateTo(getTargetPath()));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
export const getFetchConfigurationAsync = createAsyncThunk(
|
||||
"login/getFetchConfigurationAsync",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
return api
|
||||
.invoke("GET", "/api/v1/login")
|
||||
.then((loginDetails: ILoginDetails) => {
|
||||
return loginDetails;
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const getVersionAsync = createAsyncThunk(
|
||||
"login/getVersionAsync",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
return api
|
||||
.invoke("GET", "/api/v1/check-version")
|
||||
.then(
|
||||
({
|
||||
current_version,
|
||||
latest_version,
|
||||
}: {
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
}) => {
|
||||
return latest_version;
|
||||
}
|
||||
)
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
// try the operator version
|
||||
api
|
||||
.invoke("GET", "/api/v1/check-operator-version")
|
||||
.then(
|
||||
({
|
||||
current_version,
|
||||
latest_version,
|
||||
}: {
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
}) => {
|
||||
return latest_version;
|
||||
}
|
||||
)
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
return err;
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -17,6 +17,7 @@ import { useDispatch } from "react-redux";
|
||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
import systemReducer from "./systemSlice";
|
||||
import loginReducer from "./screens/LoginPage/loginSlice";
|
||||
import traceReducer from "./screens/Console/Trace/traceSlice";
|
||||
import logReducer from "./screens/Console/Logs/logsSlice";
|
||||
import healthInfoReducer from "./screens/Console/HealthInfo/healthInfoSlice";
|
||||
@@ -36,6 +37,7 @@ import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetai
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
system: systemReducer,
|
||||
login: loginReducer,
|
||||
trace: traceReducer,
|
||||
logs: logReducer,
|
||||
watch: watchReducer,
|
||||
|
||||
Reference in New Issue
Block a user