From be054fe4ce4501f7e0fcf690153c9a4c8146000d Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Mon, 24 Jan 2022 15:40:38 -0800 Subject: [PATCH] License Page UI Updates (#1444) * License Page UI Updates Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> * Lint Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- pkg/subnet/config.go | 6 - pkg/subnet/subnet.go | 71 +- .../src/screens/Console/License/License.tsx | 961 ++++++------------ .../screens/Console/License/LicensePlans.tsx | 377 +++++++ .../src/screens/Console/License/utils.ts | 212 +++- .../src/screens/Console/Support/Register.tsx | 63 +- .../Console/Support/RegisterStatus.tsx | 62 ++ restapi/admin_subnet.go | 62 +- 8 files changed, 1060 insertions(+), 754 deletions(-) create mode 100644 portal-ui/src/screens/Console/License/LicensePlans.tsx create mode 100644 portal-ui/src/screens/Console/Support/RegisterStatus.tsx diff --git a/pkg/subnet/config.go b/pkg/subnet/config.go index 1f06b9625..94a087c48 100644 --- a/pkg/subnet/config.go +++ b/pkg/subnet/config.go @@ -20,15 +20,9 @@ import ( "errors" "log" - "github.com/minio/pkg/env" "github.com/minio/pkg/licverifier" ) -// GetSubnetURL -func GetSubnetURL() string { - return env.Get(ConsoleSubnetURL, "https://subnet.min.io") -} - // GetLicenseInfoFromJWT will return license metadata from a jwt string license func GetLicenseInfoFromJWT(license string, publicKeys []string) (*licverifier.LicenseInfo, error) { if license == "" { diff --git a/pkg/subnet/subnet.go b/pkg/subnet/subnet.go index 88f4a0469..75c91dc4f 100644 --- a/pkg/subnet/subnet.go +++ b/pkg/subnet/subnet.go @@ -18,10 +18,14 @@ package subnet import ( + "bytes" "encoding/json" "errors" + "fmt" "log" + "github.com/minio/pkg/licverifier" + "github.com/minio/console/models" "github.com/minio/madmin-go" mc "github.com/minio/mc/cmd" @@ -81,7 +85,12 @@ func GetOrganizations(client cluster.HTTPClientI, token string) ([]*models.Subne return organizations, nil } -func Register(client cluster.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (string, error) { +type LicenseTokenConfig struct { + APIKey string + License string +} + +func Register(client cluster.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) { var headers map[string]string regInfo := GetClusterRegInfo(admInfo) regURL := subnetRegisterURL() @@ -89,23 +98,71 @@ func Register(client cluster.HTTPClientI, admInfo madmin.InfoMessage, apiKey, to regURL += "?api_key=" + apiKey } else { if accountID == "" || token == "" { - return "", errors.New("missing accountID or authentication token") + return nil, errors.New("missing accountID or authentication token") } headers = subnetAuthHeaders(token) regURL += "?aid=" + accountID } regToken, err := GenerateRegToken(regInfo) if err != nil { - return "", err + return nil, err } reqPayload := mc.ClusterRegistrationReq{Token: regToken} resp, err := subnetPostReq(client, regURL, reqPayload, headers) + if err != nil { + return nil, err + } + respJSON := gjson.Parse(resp) + subnetAPIKey := respJSON.Get("api_key").String() + licenseJwt := respJSON.Get("license").String() + + if subnetAPIKey != "" { + return &LicenseTokenConfig{ + APIKey: subnetAPIKey, + License: licenseJwt, + }, nil + } + return nil, errors.New("subnet api key not found") +} + +const publicKey = "/downloads/license-pubkey.pem" + +// downloadSubnetPublicKey will download the current subnet public key. +func downloadSubnetPublicKey(client cluster.HTTPClientI) (string, error) { + // Get the public key directly from Subnet + url := fmt.Sprintf("%s%s", subnetBaseURL(), publicKey) + resp, err := client.Get(url) if err != nil { return "", err } - subnetAPIKey := gjson.Parse(resp).Get("api_key").String() - if subnetAPIKey != "" { - return subnetAPIKey, nil + defer resp.Body.Close() + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err } - return "", errors.New("subnet api key not found") + return buf.String(), err +} + +// ParseLicense parses the license with the bundle public key and return it's information +func ParseLicense(client cluster.HTTPClientI, license string) (*licverifier.LicenseInfo, error) { + var publicKeys []string + + subnetPubKey, err := downloadSubnetPublicKey(client) + if err != nil { + log.Print(err) + // there was an issue getting the subnet public key + // use hardcoded public keys instead + publicKeys = OfflinePublicKeys + } else { + publicKeys = append(publicKeys, subnetPubKey) + } + + licenseInfo, err := GetLicenseInfoFromJWT(license, publicKeys) + if err != nil { + fmt.Println(err) + return nil, err + } + + return licenseInfo, nil } diff --git a/portal-ui/src/screens/Console/License/License.tsx b/portal-ui/src/screens/Console/License/License.tsx index 1a0971c1e..d1fe1d4b9 100644 --- a/portal-ui/src/screens/Console/License/License.tsx +++ b/portal-ui/src/screens/Console/License/License.tsx @@ -25,15 +25,12 @@ import Grid from "@mui/material/Grid"; import Button from "@mui/material/Button"; import Moment from "react-moment"; import Typography from "@mui/material/Typography"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { SubnetInfo } from "./types"; import { AppState } from "../../../store"; import { niceBytes } from "../../../common/utils"; import { ErrorResponseHandler } from "../../../common/types"; import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; -import { planButtons, planDetails, planItems } from "./utils"; import PageHeader from "../Common/PageHeader/PageHeader"; -import ActivationModal from "./ActivationModal"; import LicenseModal from "./LicenseModal"; import api from "../../../common/api"; import { LicenseIcon } from "../../../icons"; @@ -43,6 +40,9 @@ import { IAM_PAGES, IAM_PAGES_PERMISSIONS, } from "../../../common/SecureComponent/permissions"; +import RegisterStatus from "../Support/RegisterStatus"; +import LicensePlans from "./LicensePlans"; +import { Link } from "react-router-dom"; const mapState = (state: AppState) => ({ operatorMode: state.system.operatorMode, @@ -120,118 +120,17 @@ const styles = (theme: Theme) => padding: 0, margin: 0, }, - tableContainer: { - marginLeft: 28, - }, - detailsContainer: { - textAlign: "center", - paddingBottom: 12, - borderRadius: "3px 3px 0 0", - maxWidth: "calc(25% - 8px)", - }, - detailsContainerBorder: { - borderLeft: "1px solid #e2e2e2", - }, - detailsTitle: { - fontSize: 19, - fontWeight: 700, - marginBottom: 26, - paddingTop: 18, - }, - currPlan: { - color: "white", - backgroundColor: "#4CCB92", - padding: 4, - }, - planHeader: { - background: "#FFFFFF", - borderRadius: "3px 3px 0px 0px", - padding: 8, - borderTop: "1px solid #D5DDE5", - }, - detailsPrice: { - fontSize: 13, - fontWeight: 700, - marginBottom: 8, - }, - detailsCapacityMax: { - minHeight: 28, - fontSize: 10, - fontWeight: 700, - marginBottom: 12, - padding: "0% 15%", - }, - itemContainer: { - height: 36, - "& .item:last-child": { - borderRight: "1px solid #e5e5e5", - }, - }, - itemContainerDetail: { - height: 48, - }, - item: { - height: "100%", - borderLeft: "1px solid #e5e5e5", - textAlign: "center", - fontSize: 10, - fontWeight: 700, - display: "flex", - alignItems: "center", - alignContent: "center", - maxWidth: "calc(25% - 8px)", - borderTop: "1px solid #e5e5e5", - }, - itemFirst: { - borderLeft: 0, - borderRight: 0, - }, - itemHighlighted: { - borderLeft: "1px solid #e5e5e5", - }, - field: { - textAlign: "left", - fontWeight: 400, - fontSize: 12, - }, - checkIcon: { - fontSize: 15, - color: "#385973", - }, - buttonContainer: { - paddingTop: 8, - paddingBottom: 24, - height: "100%", - display: "flex", - justifyContent: "center", - borderLeft: "1px solid #e2e2e2", - maxWidth: "calc(25% - 8px)", - }, - buttonContainerBlank: { - border: 0, - }, - buttonContainerHighlighted: { - borderTop: 0, - }, button: { textTransform: "none", fontSize: 15, fontWeight: 700, }, openSourcePolicy: { + fontSize: 14, color: "#1C5A8D", fontWeight: "bold", }, - activateLink: { - color: "#1C5A8D", - fontWeight: "bold", - clear: "both", - background: "none", - border: "none", - textDecoration: "underline", - cursor: "pointer", - }, subnetRefreshLicenseLink: { color: "#1C5A8D", fontWeight: "bold", @@ -255,20 +154,6 @@ const styles = (theme: Theme) => fontSize: 14, fontWeight: "bold", }, - currentPlanBG: { - background: "#022A4A 0% 0% no-repeat padding-box", - color: "#FFFFFF", - borderTop: "1px solid #52687d", - }, - planItemsPadding: { - border: "1px solid #EAEDEE", - borderTop: 0, - maxWidth: 1180, - }, - planItemsBorder: { - height: 7, - backgroundColor: "#07193E", - }, subnetSubTitle: { fontSize: 14, }, @@ -318,6 +203,7 @@ const License = ({ classes, operatorMode }: ILicenseProps) => { useState(true); const [loadingRefreshLicense, setLoadingRefreshLicense] = useState(false); + const [clusterRegistered, setClusterRegistered] = useState(false); const getSubnetInfo = hasPermission( CONSOLE_UI_RESOURCE, @@ -349,9 +235,11 @@ const License = ({ classes, operatorMode }: ILicenseProps) => { } setLicenseInfo(res); } + setClusterRegistered(true); setLoadingLicenseInfo(false); }) .catch(() => { + setClusterRegistered(false); setLoadingLicenseInfo(false); }); } else { @@ -395,545 +283,326 @@ const License = ({ classes, operatorMode }: ILicenseProps) => { ); } + return ( +
- - - GNU Affero General Public License - - - - - {licenseInfo ? ( - - - - - License - - - Commercial License - - - Organization - - - {licenseInfo.organization} - - - Registered Capacity - - - {niceBytes( - (licenseInfo.storage_capacity * 1099511627776) // 1 Terabyte = 1099511627776 Bytes - .toString(10), - false - )} - - - - - Subscription Plan - - - {licenseInfo.plan} - - - Requestor - - - {licenseInfo.email} - - - Expiry Date - - - - {licenseInfo.expires_at - .split(" ") - .slice(0, 1) - .join(" ")} - - - - verified - - - ) : ( - - setLicenseModal(false)} - /> - - - agpl{" "} - - - Version 3. 19 November 2007{" "} - - - - - - The GNU Affero General Public License is a free, - copyleft license for software and other kinds of - works, specifically designed to ensure cooperation - with the Community in the case of network server - software. - -
- - The licenses for most software and other practical - works are designed to take away your freedom to share - and change the works. By contrast, our General Public - Licenses are intended to guarantee your freedom to - share and change all versions of a program--to make - sure it remains free software for all its users. - -
- -
-
-
- )} + {licenseInfo && } + {!clusterRegistered && ( + + + + GNU Affero General Public License - - {licenseInfo ? ( -
- - Login to MinIO SUBNET ! - - - It combines a commercial license with a support - experience unlike any other. - -
- - {operatorMode && ( - - {" "} -
-
- - {loadingRefreshLicense && ( - - )} -
- )} -
- ) : ( -
- - Choosing between GNU AGPL v3 and Commercial License - -
- - If you are building proprietary applications, you may - want to choose the commercial license included as part - of the Standard and Enterprise subscription plans. - Applications must otherwise comply with all the GNU - AGPLv3 License & Trademark obligations. Follow the links - below to learn more about the compliance policy. - - -
-
- )} - - - - -
-
- - Are you already a customer? - - -
-
- -
- - - - closeModalAndFetchLicenseInfo()} - /> - - - - {planDetails.map((details: any) => { - let currentPlan = - (!licenseInfo && details.title === "Community") || - (licenseInfo && - licenseInfo.plan.toLowerCase() === - details.title.toLowerCase()); - return ( - - - - {details.title} - - - {currentPlan ? ( - - CURRENT PLAN - - ) : ( - details.price - )} - - - {details.capacityMax || ""} - - - ); - })} - - {planItems.map((item: any) => { - return ( - - - {item.field} - - - - {item.community === "N/A" ? ( - "" - ) : item.community === "Yes" ? ( - - ) : ( - - {item.communityLink !== undefined && - item.communityLink ? ( - - ) : ( - item.community - )} - - )} - - {item.communityDetail !== undefined && ( - - {item.communityDetail} - - )} - - - - {item.standard === "N/A" ? ( - "" - ) : item.standard === "Yes" ? ( - - ) : ( - item.standard - )} - - {item.standardDetail !== undefined && ( - - {item.standardDetail} - - )} - - - - {item.enterprise === "N/A" ? ( - "" - ) : item.enterprise === "Yes" ? ( - - ) : ( - item.enterprise - )} - - {item.enterpriseDetail !== undefined && ( - - {item.enterpriseDetail} - - )} - - - ); - })} - + + - {planButtons.map((button: any, index: any) => { - return ( - - - + xs={12} + lg={12} + className={`${classes.licenseContainer}`} + > + {licenseInfo ? ( + + + + + License + + + Commercial License + + + Organization + + + {licenseInfo.organization} + + + Registered Capacity + + + {niceBytes( + (licenseInfo.storage_capacity * 1099511627776) // 1 Terabyte = 1099511627776 Bytes + .toString(10), + false + )} + + + + + Subscription Plan + + + {licenseInfo.plan} + + + Requestor + + + {licenseInfo.email} + + + Expiry Date + + + + {licenseInfo.expires_at + .split(" ") + .slice(0, 1) + .join(" ")} + + + + verified - {button.text === "Subscribe" && - !( - licenseInfo && - licenseInfo.plan.toLowerCase() === - button.plan.toLowerCase() - ) && ( - - - - )} - - ); - })} + + ) : ( + + setLicenseModal(false)} + /> + + + agpl{" "} + + + Version 3. 19 November 2007{" "} + + + + + + The GNU Affero General Public License is a free, + copyleft license for software and other kinds of + works, specifically designed to ensure cooperation + with the Community in the case of network server + software. + +
+ + The licenses for most software and other practical + works are designed to take away your freedom to + share and change the works. By contrast, our + General Public Licenses are intended to guarantee + your freedom to share and change all versions of a + program--to make sure it remains free software for + all its users. + +
+ +
+
+
+ )} +
+ + {licenseInfo ? ( +
+ + Login to MinIO SUBNET ! + + + It combines a commercial license with a support + experience unlike any other. + +
+ + {operatorMode && ( + + {" "} +
+
+ + {loadingRefreshLicense && ( + + )} +
+ )} +
+ ) : ( +
+ + Choosing between GNU AGPL v3 and Commercial License + +
+ + If you are building proprietary applications, you + may want to choose the commercial license included + as part of the Standard and Enterprise subscription + plans. Applications must otherwise comply with all + the GNU AGPLv3 License & Trademark obligations. + Follow the links below to learn more about the + compliance policy. + + +
+
+ )} + - - + +
+
+ + + Are you already a customer? Register Here → + + + +
+
+ + )} + +
diff --git a/portal-ui/src/screens/Console/License/LicensePlans.tsx b/portal-ui/src/screens/Console/License/LicensePlans.tsx new file mode 100644 index 000000000..84e63849e --- /dev/null +++ b/portal-ui/src/screens/Console/License/LicensePlans.tsx @@ -0,0 +1,377 @@ +// 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 } from "react"; +import Grid from "@mui/material/Grid"; +import clsx from "clsx"; +import ActivationModal from "./ActivationModal"; +import { planButtons, planDetails, planItems } from "./utils"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import Button from "@mui/material/Button"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import { SubnetInfo } from "./types"; +import withStyles from "@mui/styles/withStyles"; + +const styles = (theme: Theme) => + createStyles({ + planItemsPadding: { + border: "1px solid #EAEDEE", + borderTop: 0, + maxWidth: 1180, + }, + planItemsBorder: { + height: 7, + backgroundColor: "#07193E", + }, + + link: { + textDecoration: "underline !important", + color: theme.palette.info.main, + }, + linkButton: { + fontFamily: '"Lato", sans-serif', + fontWeight: "normal", + textTransform: "none", + fontSize: "inherit", + height: 0, + padding: 0, + margin: 0, + }, + tableContainer: { + marginLeft: 28, + }, + detailsContainerBorder: { + borderLeft: "1px solid #e2e2e2", + }, + detailsTitle: { + fontSize: 19, + fontWeight: 700, + marginBottom: 26, + paddingTop: 18, + lineHeight: 1, + }, + currPlan: { + color: "white", + backgroundColor: "#4CCB92", + }, + planHeader: { + padding: 8, + }, + detailsPrice: { + fontSize: 13, + fontWeight: 700, + }, + detailsCapacityMax: { + minHeight: 28, + fontSize: 10, + }, + itemContainer: { + height: 36, + "& .item:last-child": { + borderRight: "1px solid #e5e5e5", + }, + }, + itemContainerDetail: { + height: 48, + }, + item: { + height: "100%", + borderLeft: "1px solid #e5e5e5", + textAlign: "center", + fontSize: 10, + fontWeight: 700, + display: "flex", + alignItems: "center", + alignContent: "center", + borderTop: "1px solid #e5e5e5", + }, + + itemFirst: { + borderLeft: 0, + borderRight: 0, + }, + field: { + textAlign: "left", + fontWeight: 400, + fontSize: 12, + }, + checkIcon: { + fontSize: 15, + color: "#385973", + }, + buttonContainer: { + paddingTop: 8, + paddingBottom: 24, + height: "100%", + display: "flex", + justifyContent: "center", + borderLeft: "1px solid #e2e2e2", + }, + buttonContainerBlank: { + border: 0, + }, + buttonContainerHighlighted: { + borderTop: 0, + }, + button: { + textTransform: "none", + fontSize: 15, + fontWeight: 700, + }, + activateLink: { + color: "#1C5A8D", + fontWeight: "bold", + clear: "both", + background: "none", + border: "none", + textDecoration: "underline", + cursor: "pointer", + }, + currentPlanBG: { + background: "#022A4A 0% 0% no-repeat padding-box", + color: "#FFFFFF", + borderTop: "1px solid #52687d", + }, + }); + +interface IRegisterStatus { + classes: any; + activateProductModal: any; + closeModalAndFetchLicenseInfo: any; + licenseInfo: SubnetInfo | undefined; + setLicenseModal: React.Dispatch>; + operatorMode: boolean; + currentPlanID: number; + setActivateProductModal: any; +} + +const LicensePlans = ({ + classes, + activateProductModal, + closeModalAndFetchLicenseInfo, + licenseInfo, + setLicenseModal, + operatorMode, + currentPlanID, + setActivateProductModal, +}: IRegisterStatus) => { + const planDetailsFiltered = planDetails.filter((item) => { + if (licenseInfo) { + if (item.title === "Community") { + return false; + } + } + return true; + }); + + const planButtonsFiltered = planButtons.filter((item) => { + if (licenseInfo) { + if (item.plan === "Community") { + return false; + } + } + return true; + }); + + const gridColWidth = licenseInfo ? 4 : 3; + + return ( + + +
+ + + + closeModalAndFetchLicenseInfo()} + /> + + + + {planDetailsFiltered.map((details: any) => { + let currentPlan = + (!licenseInfo && details.title === "Community") || + (licenseInfo && + licenseInfo.plan.toLowerCase() === + details.title.toLowerCase()); + return ( + + + + {details.title} + {currentPlan && ( + +
CURRENT PLAN
+
+ )} +
+ + {details.price} + + + {details.capacityMax || ""} + +
+ ); + })} +
+ {planItems.map((item: any) => { + return ( + + + {item.field} + + {planDetailsFiltered.map((pd) => { + return ( + + + + {item.plans[pd.title].label === "N/A" ? ( + "" + ) : item.plans[pd.title].label === "Yes" ? ( + + ) : ( + + {item.plans[pd.title].link ? ( + + ) : ( + item.plans[pd.title].label + )} + + )} + + {item.plans[pd.title].detail !== undefined && ( + + {item.plans[pd.title].detail} + + )} + + + ); + })} + + ); + })} + + + {planButtonsFiltered.map((button: any, index: any) => { + return ( + + + + + + ); + })} + + +
+
+ + ); +}; + +export default withStyles(styles)(LicensePlans); diff --git a/portal-ui/src/screens/Console/License/utils.ts b/portal-ui/src/screens/Console/License/utils.ts index 0336cac32..4473dd543 100644 --- a/portal-ui/src/screens/Console/License/utils.ts +++ b/portal-ui/src/screens/Console/License/utils.ts @@ -14,12 +14,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -export const planDetails = [ +export interface IPlanDetails { + id: number; + title: string; + price: string; + capacityMax: string; + capacityMin?: string; +} + +export const planDetails: IPlanDetails[] = [ { id: 0, title: "Community", - price: "N/A", - capacityMax: "Open Source", + price: "Open Source", + capacityMax: "", }, { id: 1, @@ -37,113 +45,221 @@ export const planDetails = [ }, ]; -export const planItems = [ +export interface IPlanItemValue { + label: string; + detail?: string; + link?: boolean; +} + +export interface IPlanItemValues { + [index: string]: IPlanItemValue; +} +export interface IPlanItem { + id: number; + field: string; + plans: IPlanItemValues; +} + +export const planItems: IPlanItem[] = [ { id: 0, field: "License", - community: "GNU AGPL v3", - communityLink: true, - communityDetail: "", - standard: "Commercial License", - standardDetail: "", - enterprise: "Commercial License", - enterpriseDetail: "", + plans: { + Community: { + label: "GNU AGPL v3", + detail: "", + link: true, + }, + Standard: { + label: "Commercial License", + }, + Enterprise: { + label: "Commercial License", + }, + }, }, { id: 1, field: "Software Release", - community: "Upstream", - standard: "1 Year Long Term Support", - enterprise: "5 Years Long Term Support", + plans: { + Community: { + label: "Upstream", + }, + Standard: { + label: "1 Year Long Term Support", + }, + Enterprise: { + label: "5 Years Long Term Support", + }, + }, }, { id: 2, field: "SLA", - community: "No SLA", - standard: "<48 Hours (Local Business Hours)", - enterprise: "<1 hour", + plans: { + Community: { + label: "No SLA", + }, + Standard: { + label: "<48 Hours (Local Business Hours)", + }, + Enterprise: { + label: "<1 hour", + }, + }, }, { id: 3, field: "Support", - community: "Community:", - communityDetail: "Public Slack Channel + Github Issues", - standard: "L4 Direct Engineering", - standardDetail: " support via SUBNET", - enterprise: "L4 Direct Engineering", - enterpriseDetail: "support via SUBNET", + plans: { + Community: { + label: "Community:", + detail: "Public Slack Channel + Github Issues", + }, + Standard: { + label: "L4 Direct Engineering", + detail: " support via SUBNET", + }, + Enterprise: { + label: "L4 Direct Engineering", + detail: "support via SUBNET", + }, + }, }, { id: 4, field: "Security Updates & Critical Bugs", - community: "Self Update", - standard: "Continuous Scan and Alert", - enterprise: "Continuous Scan and Alert", + plans: { + Community: { + label: "Self Update", + }, + Standard: { + label: "Continuous Scan and Alert", + }, + Enterprise: { + label: "Continuous Scan and Alert", + }, + }, }, { id: 5, field: "Panic Button", - community: "N/A", - standard: "1 per year", - enterprise: "Unlimited", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "1 per year", + }, + Enterprise: { + label: "Unlimited", + }, + }, }, { id: 6, field: "Health Diagnostics", - community: "N/A", - standard: "24/7/365", - enterprise: "24/7/365", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "24/7/365", + }, + Enterprise: { + label: "24/7/365", + }, + }, }, { id: 6, field: "Annual Architecture Review", - community: "N/A", - standard: "N/A", - enterprise: "Yes", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "N/A", + }, + Enterprise: { + label: "Yes", + }, + }, }, { id: 7, field: "Annual Performance Review", - community: "N/A", - standard: "N/A", - enterprise: "Yes", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "N/A", + }, + Enterprise: { + label: "Yes", + }, + }, }, { id: 8, field: "Indemnification", - community: "N/A", - standard: "N/A", - enterprise: "Yes", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "N/A", + }, + Enterprise: { + label: "Yes", + }, + }, }, { id: 9, field: "Security + Policy Review", - community: "N/A", - standard: "N/A", - enterprise: "Yes", + plans: { + Community: { + label: "N/A", + }, + Standard: { + label: "N/A", + }, + Enterprise: { + label: "Yes", + }, + }, }, ]; -export const planButtons = [ +export interface IPlanButton { + id: number; + text: string; + text2: string; + link: string; + plan: string; +} +export const planButtons: IPlanButton[] = [ { id: 0, text: "Join Slack", text2: "", link: "https://slack.min.io", - plan: "community", + plan: "Community", }, { id: 1, text: "Subscribe", text2: "Sign up", link: "https://subnet.min.io/subscription", - plan: "standard", + plan: "Standard", }, { id: 2, text: "Subscribe", text2: "Sign up", link: "https://subnet.min.io/subscription", - plan: "enterprise", + plan: "Enterprise", }, ]; diff --git a/portal-ui/src/screens/Console/Support/Register.tsx b/portal-ui/src/screens/Console/Support/Register.tsx index 2b65a3f68..959ba5c2b 100644 --- a/portal-ui/src/screens/Console/Support/Register.tsx +++ b/portal-ui/src/screens/Console/Support/Register.tsx @@ -18,8 +18,8 @@ import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; import { actionsTray, - searchField, containerForHeader, + searchField, } from "../Common/FormComponents/common/styleLibrary"; import withStyles from "@mui/styles/withStyles"; import { Button, Grid, Link, Typography } from "@mui/material"; @@ -30,7 +30,6 @@ import { CopyIcon, UsersIcon } from "../../../icons"; import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; import OnlineRegistrationIcon from "../../../icons/OnlineRegistrationIcon"; -import OfflineRegistrationBackIcon from "../../../icons/OfflineRegistrationBackIcon"; import OfflineRegistrationIcon from "../../../icons/OfflineRegistrationIcon"; import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import clsx from "clsx"; @@ -55,9 +54,11 @@ import { IAM_PAGES, IAM_PAGES_PERMISSIONS, } from "../../../common/SecureComponent/permissions"; -import VerifiedIcon from "../../../icons/VerifiedIcon"; import { connect } from "react-redux"; import { setErrorSnackMessage } from "../../../actions"; +import HelpBox from "../../../common/HelpBox"; +import SettingsIcon from "../../../icons/SettingsIcon"; +import RegisterStatus from "./RegisterStatus"; interface IRegister { classes: any; @@ -201,7 +202,9 @@ const Register = ({ classes, displayErrorMessage }: IRegister) => { setLoadingLicenseInfo(false); }) .catch((err: ErrorResponseHandler) => { - displayErrorMessage(err); + if (err.errorMessage !== "License not found") { + displayErrorMessage(err); + } setClusterRegistered(false); setLoadingLicenseInfo(false); }); @@ -224,6 +227,7 @@ const Register = ({ classes, displayErrorMessage }: IRegister) => { } }) .catch((err: ErrorResponseHandler) => { + console.log(err); displayErrorMessage(err); setLoading(false); }); @@ -440,7 +444,7 @@ const Register = ({ classes, displayErrorMessage }: IRegister) => { target="_blank" href="https://min.io/product/subnet" > - Learn more about SUBNET + Learn more about SUBNET.

@@ -563,33 +567,14 @@ const Register = ({ classes, displayErrorMessage }: IRegister) => { - {clusterRegistered && ( - - - Register Status: - - Registered - - - )} + {clusterRegistered && } {title} {onlineActivation ? ( - - - { - fetchSubnetRegToken(); - setOnlineActivation(!onlineActivation); - }} - > - Offline Activation - - + ) : ( @@ -606,6 +591,32 @@ const Register = ({ classes, displayErrorMessage }: IRegister) => { {clusterRegistrationForm} + {onlineActivation && ( + + } + help={ + + For airgap/firewalled environments it is possible to configure + a proxy to connect to Subnet. +
+
+ Alternatively you can try{" "} + { + fetchSubnetRegToken(); + setOnlineActivation(!onlineActivation); + }} + > + Offline Activation. + +
+ } + /> +
+ )}
); diff --git a/portal-ui/src/screens/Console/Support/RegisterStatus.tsx b/portal-ui/src/screens/Console/Support/RegisterStatus.tsx new file mode 100644 index 000000000..25566e1ec --- /dev/null +++ b/portal-ui/src/screens/Console/Support/RegisterStatus.tsx @@ -0,0 +1,62 @@ +// 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 { Grid } from "@mui/material"; +import VerifiedIcon from "../../../icons/VerifiedIcon"; +import React from "react"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; + +const styles = (theme: Theme) => + createStyles({ + registeredStatus: { + border: "1px solid #E2E2E2", + padding: "24px 24px 24px 24px", + borderRadius: 2, + marginBottom: 25, + backgroundColor: "#FBFAFA", + "& .min-icon": { + width: 20, + height: 20, + marginLeft: 48, + marginRight: 13, + verticalAlign: "middle", + marginTop: -3, + }, + "& span": { + fontWeight: "bold", + }, + }, + }); + +interface IRegisterStatus { + classes: any; +} + +function RegisterStatus({ classes }: IRegisterStatus) { + return ( + + + Register Status: + + Registered + + + ); +} + +export default withStyles(styles)(RegisterStatus); diff --git a/restapi/admin_subnet.go b/restapi/admin_subnet.go index 44f52d180..3c84d4461 100644 --- a/restapi/admin_subnet.go +++ b/restapi/admin_subnet.go @@ -20,6 +20,7 @@ package restapi import ( "context" "errors" + "fmt" "github.com/go-openapi/runtime/middleware" "github.com/minio/console/cluster" @@ -57,11 +58,14 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) { }) // Get subnet info api.AdminAPISubnetInfoHandler = admin_api.SubnetInfoHandlerFunc(func(params admin_api.SubnetInfoParams, session *models.Principal) middleware.Responder { - err := GetSubnetInfoResponse(session) + client := &cluster.HTTPClient{ + Client: GetConsoleHTTPClient(), + } + resp, err := GetSubnetInfoResponse(session, client) if err != nil { return admin_api.NewSubnetInfoDefault(int(err.Code)).WithPayload(err) } - return admin_api.NewSubnetInfoOK() + return admin_api.NewSubnetInfoOK().WithPayload(resp) }) // Get subnet registration token api.AdminAPISubnetRegTokenHandler = admin_api.SubnetRegTokenHandlerFunc(func(params admin_api.SubnetRegTokenParams, session *models.Principal) middleware.Responder { @@ -78,11 +82,11 @@ func SubnetRegisterWithAPIKey(ctx context.Context, minioClient MinioAdmin, apiKe if err != nil { return false, err } - subnetAPIKey, err := subnet.Register(httpClient, serverInfo, apiKey, "", "") + registerResult, err := subnet.Register(httpClient, serverInfo, apiKey, "", "") if err != nil { return false, err } - configStr := "subnet license= api_key=" + subnetAPIKey + configStr := fmt.Sprintf("subnet license=%s api_key=%s", registerResult.License, registerResult.APIKey) _, err = minioClient.setConfigKV(ctx, configStr) if err != nil { return false, err @@ -179,26 +183,28 @@ func GetSubnetLoginWithMFAResponse(params admin_api.SubnetLoginMFAParams) (*mode return resp, nil } -func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin, key string) (string, error) { +func GetSubnetKeyFromMinIOConfig(ctx context.Context, minioClient MinioAdmin) (*subnet.LicenseTokenConfig, error) { sh, err := minioClient.helpConfigKV(ctx, "subnet", "", false) if err != nil { - return "", err + return nil, err } buf, err := minioClient.getConfigKV(ctx, "subnet") if err != nil { - return "", err + return nil, err } tgt, err := madmin.ParseSubSysTarget(buf, sh) if err != nil { - return "", err + return nil, err } - + res := subnet.LicenseTokenConfig{} for _, kv := range tgt.KVS { - if kv.Key == key { - return kv.Value, nil + if kv.Key == "api_key" { + res.APIKey = kv.Value + } else if kv.Key == "license" { + res.License = kv.Value } } - return "", errors.New("") + return &res, nil } func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient cluster.HTTPClientI, params admin_api.SubnetRegisterParams) error { @@ -206,11 +212,11 @@ func GetSubnetRegister(ctx context.Context, minioClient MinioAdmin, httpClient c if err != nil { return err } - subnetAPIKey, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID) + registerResult, err := subnet.Register(httpClient, serverInfo, "", *params.Body.Token, *params.Body.AccountID) if err != nil { return err } - configStr := "subnet license= api_key=" + subnetAPIKey + configStr := fmt.Sprintf("subnet license=%s api_key=%s", registerResult.License, registerResult.APIKey) _, err = minioClient.setConfigKV(ctx, configStr) if err != nil { return err @@ -235,21 +241,35 @@ func GetSubnetRegisterResponse(session *models.Principal, params admin_api.Subne return nil } -func GetSubnetInfoResponse(session *models.Principal) *models.Error { +func GetSubnetInfoResponse(session *models.Principal, client cluster.HTTPClientI) (*models.License, *models.Error) { + fmt.Println("quack") ctx := context.Background() mAdmin, err := NewMinioAdminClient(session) if err != nil { - return prepareError(err) + return nil, prepareError(err) } adminClient := AdminClient{Client: mAdmin} - apiKey, err := GetSubnetKeyFromMinIOConfig(ctx, adminClient, "api_key") + subnetTokens, err := GetSubnetKeyFromMinIOConfig(ctx, adminClient) if err != nil { - return prepareError(err) + return nil, prepareError(err) } - if apiKey == "" { - return prepareError(errLicenseNotFound) + if subnetTokens.APIKey == "" { + return nil, prepareError(errLicenseNotFound) } - return nil + licenseInfo, err := subnet.ParseLicense(client, subnetTokens.License) + if err != nil { + fmt.Println(err) + } + license := &models.License{ + Email: licenseInfo.Email, + AccountID: licenseInfo.AccountID, + StorageCapacity: licenseInfo.StorageCapacity, + Plan: licenseInfo.Plan, + ExpiresAt: licenseInfo.ExpiresAt.String(), + Organization: licenseInfo.Organization, + } + return license, nil + } func GetSubnetRegToken(ctx context.Context, minioClient MinioAdmin) (string, error) {