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;