diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts
index 57dff1334..f80cc3b8b 100644
--- a/portal-ui/src/common/SecureComponent/permissions.ts
+++ b/portal-ui/src/common/SecureComponent/permissions.ts
@@ -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
diff --git a/portal-ui/src/common/api/index.ts b/portal-ui/src/common/api/index.ts
index 0ebe7643a..ff7f6bfbb 100644
--- a/portal-ui/src/common/api/index.ts
+++ b/portal-ui/src/common/api/index.ts
@@ -67,6 +67,7 @@ export class API {
const throwMessage: ErrorResponseHandler = {
errorMessage: capMessage,
detailedError: capDetailed,
+ statusCode: err.status,
};
return Promise.reject(throwMessage);
diff --git a/portal-ui/src/common/types.ts b/portal-ui/src/common/types.ts
index de9636b0c..969d763bd 100644
--- a/portal-ui/src/common/types.ts
+++ b/portal-ui/src/common/types.ts
@@ -449,6 +449,7 @@ export interface AffinityConfiguration {
export interface ErrorResponseHandler {
errorMessage: string;
detailedError: string;
+ statusCode?: number;
}
export interface IRetentionConfig {
diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx
index 15a0eee82..15700e443 100644
--- a/portal-ui/src/screens/Console/Console.tsx
+++ b/portal-ui/src/screens/Console/Console.tsx
@@ -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 = (
diff --git a/portal-ui/src/screens/Console/Marketplace/Marketplace.tsx b/portal-ui/src/screens/Console/Marketplace/Marketplace.tsx
new file mode 100644
index 000000000..e09ccd0c0
--- /dev/null
+++ b/portal-ui/src/screens/Console/Marketplace/Marketplace.tsx
@@ -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 .
+
+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(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 ;
+ }
+
+ if (features) {
+ return (
+
+
+
+
+
+
+ );
+ }
+ return null;
+};
+
+export default Marketplace;
diff --git a/portal-ui/src/screens/Console/Marketplace/SetEmailModal.tsx b/portal-ui/src/screens/Console/Marketplace/SetEmailModal.tsx
new file mode 100644
index 000000000..69f241903
--- /dev/null
+++ b/portal-ui/src/screens/Console/Marketplace/SetEmailModal.tsx
@@ -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 .
+
+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("");
+ const [isEmailSet, setIsEmailSet] = useState(false);
+
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ let v = event.target.value;
+ setIsEmailSet(reEmail.test(v));
+ setEmail(v);
+ };
+
+ const onConfirm = () => {
+ invokeApi("POST", "/api/v1/mp-integration", { email });
+ };
+
+ return open ? (
+ }
+ isLoading={isLoading}
+ cancelText={"Later"}
+ onConfirm={onConfirm}
+ onClose={closeModal}
+ confirmButtonProps={{
+ color: "info",
+ disabled: !isEmailSet || isLoading,
+ }}
+ confirmationContent={
+
+ Would you like to register an email for your account?
+
+
+ }
+ />
+ ) : null;
+};
+
+export default withStyles(styles)(SetEmailModal);
diff --git a/portal-ui/src/screens/Console/Marketplace/types.tsx b/portal-ui/src/screens/Console/Marketplace/types.tsx
new file mode 100644
index 000000000..74f4192c0
--- /dev/null
+++ b/portal-ui/src/screens/Console/Marketplace/types.tsx
@@ -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 .
+
+// export interface IMarketplaceProps {
+// showModal: string;
+// namespace: string;
+// pvcName: string;
+// propLoading: boolean;
+// }
+
+export interface ISetEmailModalProps {
+ open: boolean;
+ closeModal: () => void;
+}
diff --git a/portal-ui/src/screens/LoginPage/LoginPage.tsx b/portal-ui/src/screens/LoginPage/LoginPage.tsx
index 4091b5e3a..393de1df1 100644
--- a/portal-ui/src/screens/LoginPage/LoginPage.tsx
+++ b/portal-ui/src/screens/LoginPage/LoginPage.tsx
@@ -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("");
const [loadingVersion, setLoadingVersion] = useState(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) => {
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 ? : ;
const hyperLink = isOperator
diff --git a/portal-ui/src/systemSlice.ts b/portal-ui/src/systemSlice.ts
index 99da65957..8e7a407a4 100644
--- a/portal-ui/src/systemSlice.ts
+++ b/portal-ui/src/systemSlice.ts
@@ -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) => {
state.loggedIn = action.payload;
},
+ showMarketplace: (state, action: PayloadAction) => {
+ state.showMarketplace = action.payload;
+ },
operatorMode: (state, action: PayloadAction) => {
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;