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:
Javier Adriel
2022-06-23 14:22:38 -05:00
committed by GitHub
parent f3d6638384
commit ba4103e03f
9 changed files with 275 additions and 13 deletions

View File

@@ -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

View File

@@ -67,6 +67,7 @@ export class API {
const throwMessage: ErrorResponseHandler = {
errorMessage: capMessage,
detailedError: capDetailed,
statusCode: err.status,
};
return Promise.reject(throwMessage);

View File

@@ -449,6 +449,7 @@ export interface AffinityConfiguration {
export interface ErrorResponseHandler {
errorMessage: string;
detailedError: string;
statusCode?: number;
}
export interface IRetentionConfig {

View File

@@ -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 = (

View 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;

View 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);

View 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;
}

View File

@@ -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

View File

@@ -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;