Prompt email after login (#2108)
* Add new route to marketplace modal * Add redux logic for showing and displaying marketplace modal * Redirect to marketplace view if console is in operator and marketplace mode * Add marketplace component * Use navigate instead of redirect
This commit is contained in:
@@ -211,6 +211,7 @@ export const IAM_PAGES = {
|
||||
NAMESPACE_TENANT_EVENTS:
|
||||
"/namespaces/:tenantNamespace/tenants/:tenantName/events",
|
||||
NAMESPACE_TENANT_CSR: "/namespaces/:tenantNamespace/tenants/:tenantName/csr",
|
||||
OPERATOR_MARKETPLACE: "/marketplace",
|
||||
};
|
||||
|
||||
// roles
|
||||
|
||||
@@ -67,6 +67,7 @@ export class API {
|
||||
const throwMessage: ErrorResponseHandler = {
|
||||
errorMessage: capMessage,
|
||||
detailedError: capDetailed,
|
||||
statusCode: err.status,
|
||||
};
|
||||
|
||||
return Promise.reject(throwMessage);
|
||||
|
||||
@@ -449,6 +449,7 @@ export interface AffinityConfiguration {
|
||||
export interface ErrorResponseHandler {
|
||||
errorMessage: string;
|
||||
detailedError: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface IRetentionConfig {
|
||||
|
||||
@@ -126,6 +126,7 @@ const TenantDetails = React.lazy(
|
||||
() => import("./Tenants/TenantDetails/TenantDetails")
|
||||
);
|
||||
const License = React.lazy(() => import("./License/License"));
|
||||
const Marketplace = React.lazy(() => import("./Marketplace/Marketplace"));
|
||||
const ConfigurationOptions = React.lazy(
|
||||
() => import("./Configurations/ConfigurationPanels/ConfigurationOptions")
|
||||
);
|
||||
@@ -451,6 +452,11 @@ const Console = ({ classes }: IConsoleProps) => {
|
||||
path: IAM_PAGES.LICENSE,
|
||||
forceDisplay: true,
|
||||
},
|
||||
{
|
||||
component: Marketplace,
|
||||
path: IAM_PAGES.OPERATOR_MARKETPLACE,
|
||||
forceDisplay: true,
|
||||
},
|
||||
];
|
||||
|
||||
const allowedRoutes = (
|
||||
|
||||
84
portal-ui/src/screens/Console/Marketplace/Marketplace.tsx
Normal file
84
portal-ui/src/screens/Console/Marketplace/Marketplace.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import SetEmailModal from "./SetEmailModal";
|
||||
import PageLayout from "../Common/Layout/PageLayout";
|
||||
import { selFeatures } from "../consoleSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { resourcesConfigurations } from "../Tenants/AddTenant/Steps/TenantResources/utils";
|
||||
import { selShowMarketplace, showMarketplace } from "../../../systemSlice";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppDispatch } from "../../../store";
|
||||
|
||||
const Marketplace = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const features = useSelector(selFeatures);
|
||||
const displayMarketplace = useSelector(selShowMarketplace);
|
||||
const [isMPMode, setMPMode] = useState<boolean>(true);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let mpMode = false;
|
||||
if (features && features.length !== 0) {
|
||||
features.forEach((feature) => {
|
||||
if (feature in resourcesConfigurations) {
|
||||
mpMode = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
setMPMode(mpMode);
|
||||
}, [features, displayMarketplace]);
|
||||
|
||||
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 closeModal = () => {
|
||||
dispatch(showMarketplace(false));
|
||||
navigate(getTargetPath());
|
||||
}
|
||||
|
||||
if (!displayMarketplace || !isMPMode) {
|
||||
return <Navigate to={{ pathname: getTargetPath() }} />;
|
||||
}
|
||||
|
||||
if (features) {
|
||||
return (
|
||||
<Fragment>
|
||||
<PageHeader label="Operator Marketplace" />
|
||||
<PageLayout>
|
||||
<SetEmailModal
|
||||
open={true}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
</PageLayout>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Marketplace;
|
||||
107
portal-ui/src/screens/Console/Marketplace/SetEmailModal.tsx
Normal file
107
portal-ui/src/screens/Console/Marketplace/SetEmailModal.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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 { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
|
||||
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
|
||||
import useApi from "../Common/Hooks/useApi";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { ISetEmailModalProps } from "./types";
|
||||
import { InfoIcon } from "../../../icons";
|
||||
import { ErrorResponseHandler } from "../../../common/types";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import { setErrorSnackMessage, setSnackBarMessage } from "../../../systemSlice";
|
||||
import { useAppDispatch } from "../../../store";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
pageTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 20,
|
||||
textAlign: "center",
|
||||
},
|
||||
pageSubTitle: {
|
||||
textAlign: "center",
|
||||
},
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const reEmail = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
const SetEmailModal = ({ open, closeModal }: ISetEmailModalProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onError = (err: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const onSuccess = (res: any) => {
|
||||
let msg = `Email ${email} has been saved`;
|
||||
dispatch(setSnackBarMessage(msg));
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const [isLoading, invokeApi] = useApi(onSuccess, onError);
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [isEmailSet, setIsEmailSet] = useState<boolean>(false);
|
||||
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = event.target.value;
|
||||
setIsEmailSet(reEmail.test(v));
|
||||
setEmail(v);
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
invokeApi("POST", "/api/v1/mp-integration", { email });
|
||||
};
|
||||
|
||||
return open ? (
|
||||
<ConfirmDialog
|
||||
title={"Register Email"}
|
||||
confirmText={"Register"}
|
||||
isOpen={open}
|
||||
titleIcon={<InfoIcon />}
|
||||
isLoading={isLoading}
|
||||
cancelText={"Later"}
|
||||
onConfirm={onConfirm}
|
||||
onClose={closeModal}
|
||||
confirmButtonProps={{
|
||||
color: "info",
|
||||
disabled: !isEmailSet || isLoading,
|
||||
}}
|
||||
confirmationContent={
|
||||
<Fragment>
|
||||
Would you like to register an email for your account?
|
||||
<InputBoxWrapper
|
||||
id="set-mp-email"
|
||||
name="set-mp-email"
|
||||
onChange={handleInputChange}
|
||||
label=""
|
||||
type={"email"}
|
||||
value={email}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default withStyles(styles)(SetEmailModal);
|
||||
27
portal-ui/src/screens/Console/Marketplace/types.tsx
Normal file
27
portal-ui/src/screens/Console/Marketplace/types.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// export interface IMarketplaceProps {
|
||||
// showModal: string;
|
||||
// namespace: string;
|
||||
// pvcName: string;
|
||||
// propLoading: boolean;
|
||||
// }
|
||||
|
||||
export interface ISetEmailModalProps {
|
||||
open: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import { SupportMenuIcon } from "../../icons/SidebarMenus";
|
||||
import GithubIcon from "../../icons/GithubIcon";
|
||||
import clsx from "clsx";
|
||||
import Loader from "../Console/Common/Loader/Loader";
|
||||
import { setErrorSnackMessage, userLogged } from "../../systemSlice";
|
||||
import { setErrorSnackMessage, userLogged, showMarketplace } from "../../systemSlice";
|
||||
import { useAppDispatch } from "../../store";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -304,6 +304,10 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
const [latestMinIOVersion, setLatestMinIOVersion] = useState<string>("");
|
||||
const [loadingVersion, setLoadingVersion] = useState<boolean>(true);
|
||||
|
||||
const isOperator =
|
||||
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
|
||||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
|
||||
|
||||
const loginStrategyEndpoints: LoginStrategyRoutes = {
|
||||
form: "/api/v1/login",
|
||||
"service-account": "/api/v1/login/operator",
|
||||
@@ -317,6 +321,38 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoginSending(true);
|
||||
@@ -332,15 +368,11 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
if (loginStrategy.loginStrategy === loginStrategyType.form) {
|
||||
localStorage.setItem("userLoggedIn", accessKey);
|
||||
}
|
||||
let targetPath = "/";
|
||||
if (
|
||||
localStorage.getItem("redirect-path") &&
|
||||
localStorage.getItem("redirect-path") !== ""
|
||||
) {
|
||||
targetPath = `${localStorage.getItem("redirect-path")}`;
|
||||
localStorage.setItem("redirect-path", "");
|
||||
if (isOperator) {
|
||||
redirectToMarketplace();
|
||||
} else {
|
||||
redirectAfterLogin();
|
||||
}
|
||||
navigate(targetPath);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoginSending(false);
|
||||
@@ -587,10 +619,6 @@ const Login = ({ classes }: ILoginProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const isOperator =
|
||||
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
|
||||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
|
||||
|
||||
const consoleText = isOperator ? <OperatorLogo /> : <ConsoleLogo />;
|
||||
|
||||
const hyperLink = isOperator
|
||||
|
||||
@@ -27,6 +27,7 @@ const initSideBarOpen = localStorage.getItem("sidebarOpen")
|
||||
export interface SystemState {
|
||||
value: number;
|
||||
loggedIn: boolean;
|
||||
showMarketplace: boolean;
|
||||
operatorMode: boolean;
|
||||
sidebarOpen: boolean;
|
||||
session: string;
|
||||
@@ -45,6 +46,7 @@ export interface SystemState {
|
||||
const initialState: SystemState = {
|
||||
value: 0,
|
||||
loggedIn: false,
|
||||
showMarketplace: false,
|
||||
operatorMode: false,
|
||||
session: "",
|
||||
userName: "",
|
||||
@@ -75,6 +77,9 @@ export const systemSlice = createSlice({
|
||||
userLogged: (state, action: PayloadAction<boolean>) => {
|
||||
state.loggedIn = action.payload;
|
||||
},
|
||||
showMarketplace: (state, action: PayloadAction<boolean>) => {
|
||||
state.showMarketplace = action.payload;
|
||||
},
|
||||
operatorMode: (state, action: PayloadAction<boolean>) => {
|
||||
state.operatorMode = action.payload;
|
||||
},
|
||||
@@ -147,6 +152,7 @@ export const systemSlice = createSlice({
|
||||
// Action creators are generated for each case reducer function
|
||||
export const {
|
||||
userLogged,
|
||||
showMarketplace,
|
||||
operatorMode,
|
||||
menuOpen,
|
||||
setServerNeedsRestart,
|
||||
@@ -165,5 +171,6 @@ export const {
|
||||
export const selDistSet = (state: AppState) => state.system.distributedSetup;
|
||||
export const selSiteRep = (state: AppState) => state.system.siteReplicationInfo;
|
||||
export const selOpMode = (state: AppState) => state.system.operatorMode;
|
||||
export const selShowMarketplace = (state: AppState) => state.system.showMarketplace;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user