github.com/minio/console@v1.4.1/web-app/src/screens/Console/License/LicensePlans.tsx (about)

     1  //  This file is part of MinIO Console Server
     2  //  Copyright (c) 2022 MinIO, Inc.
     3  //
     4  //  This program is free software: you can redistribute it and/or modify
     5  //  it under the terms of the GNU Affero General Public License as published by
     6  //  the Free Software Foundation, either version 3 of the License, or
     7  //  (at your option) any later version.
     8  //
     9  //  This program is distributed in the hope that it will be useful,
    10  //  but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  //  GNU Affero General Public License for more details.
    13  //
    14  //  You should have received a copy of the GNU Affero General Public License
    15  //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  import React, { Fragment, useEffect, useState } from "react";
    18  import clsx from "clsx";
    19  import {
    20    AGPLV3Logo,
    21    Box,
    22    breakPoints,
    23    Button,
    24    CheckCircleIcon,
    25    ConsoleEnterprise,
    26    ConsoleStandard,
    27    LicenseDocIcon,
    28  } from "mds";
    29  import { SubnetInfo } from "./types";
    30  import {
    31    COMMUNITY_PLAN_FEATURES,
    32    ENTERPRISE_PLAN_FEATURES,
    33    FEATURE_ITEMS,
    34    getRenderValue,
    35    LICENSE_PLANS,
    36    PAID_PLANS,
    37    STANDARD_PLAN_FEATURES,
    38  } from "./utils";
    39  import styled from "styled-components";
    40  import get from "lodash/get";
    41  
    42  interface IRegisterStatus {
    43    activateProductModal: any;
    44    closeModalAndFetchLicenseInfo: any;
    45    licenseInfo: SubnetInfo | undefined;
    46    currentPlanID: number;
    47    setActivateProductModal: any;
    48  }
    49  
    50  const PlanListContainer = styled.div(({ theme }) => ({
    51    display: "grid",
    52  
    53    margin: "0 1.5rem 0 1.5rem",
    54  
    55    gridTemplateColumns: "1fr 1fr 1fr 1fr",
    56  
    57    [`@media (max-width: ${breakPoints.sm}px)`]: {
    58      gridTemplateColumns: "1fr 1fr 1fr",
    59    },
    60  
    61    "&.paid-plans-only": {
    62      display: "grid",
    63      gridTemplateColumns: "1fr 1fr 1fr",
    64    },
    65  
    66    "& .features-col": {
    67      flex: 1,
    68      minWidth: "260px",
    69  
    70      "@media (max-width: 600px)": {
    71        display: "none",
    72      },
    73    },
    74  
    75    "& .xs-only": {
    76      display: "none",
    77    },
    78  
    79    "& .button-box": {
    80      display: "flex",
    81      alignItems: "center",
    82      justifyContent: "center",
    83      padding: "5px 0px 25px 0px",
    84      borderLeft: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
    85    },
    86    "& .plan-header": {
    87      height: "99px",
    88      borderBottom: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
    89    },
    90    "& .feature-title": {
    91      height: "25px",
    92      paddingLeft: "26px",
    93      fontSize: "14px",
    94  
    95      background: get(theme, "signalColors.disabled", "#E5E5E5"),
    96      color: get(theme, "signalColors.main", "#07193E"),
    97  
    98      "@media (max-width: 600px)": {
    99        "& .feature-title-info .xs-only": {
   100          display: "block",
   101        },
   102      },
   103    },
   104    "& .feature-name": {
   105      minHeight: "60px",
   106      padding: "5px",
   107      borderBottom: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
   108      display: "flex",
   109      alignItems: "center",
   110      paddingLeft: "26px",
   111      fontSize: "14px",
   112    },
   113    "& .feature-item": {
   114      display: "flex",
   115      flexFlow: "column",
   116      alignItems: "center",
   117      justifyContent: "center",
   118      minHeight: "60px",
   119      padding: "0 15px 0 15px",
   120      borderBottom: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
   121      borderLeft: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
   122      fontSize: "14px",
   123      "& .link-text": {
   124        color: "#2781B0",
   125        cursor: "pointer",
   126        textDecoration: "underline",
   127      },
   128  
   129      "&.icon-yes": {
   130        width: "15px",
   131        height: "15px",
   132      },
   133    },
   134  
   135    "& .feature-item-info": {
   136      flex: 1,
   137      display: "flex",
   138      flexFlow: "column",
   139      alignItems: "center",
   140      justifyContent: "space-around",
   141      textAlign: "center",
   142  
   143      "@media (max-width: 600px)": {
   144        justifyContent: "space-evenly",
   145        width: "100%",
   146        "& .xs-only": {
   147          display: "block",
   148        },
   149        "& .plan-feature": {
   150          textAlign: "center",
   151          paddingRight: "10px",
   152        },
   153      },
   154    },
   155  
   156    "& .plan-col": {
   157      minWidth: "260px",
   158      flex: 1,
   159    },
   160  
   161    "& .active-plan-col": {
   162      background: `${get(
   163        theme,
   164        "boxBackground",
   165        "#FDFDFD",
   166      )} 0% 0% no-repeat padding-box`,
   167      boxShadow: " 0px 3px 20px #00000038",
   168  
   169      "& .plan-header": {
   170        backgroundColor: get(theme, "signalColors.info", "#2781B0"),
   171      },
   172  
   173      "& .feature-title": {
   174        background: get(theme, "signalColors.disabled", "#E5E5E5"),
   175        color: get(theme, "fontColor", "#000"),
   176      },
   177    },
   178  }));
   179  
   180  const PlanHeaderContainer = styled.div(({ theme }) => ({
   181    display: "flex",
   182    alignItems: "flex-start",
   183    justifyContent: "center",
   184    flexFlow: "column",
   185    borderLeft: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
   186    borderBottom: "0px !important",
   187    "& .plan-header": {
   188      display: "flex",
   189      alignItems: "center",
   190      justifyContent: "center",
   191      flexFlow: "column",
   192    },
   193  
   194    "& .title-block": {
   195      display: "flex",
   196      alignItems: "center",
   197      flexFlow: "column",
   198      width: "100%",
   199      "& .title-main": {
   200        display: "flex",
   201        alignItems: "center",
   202        justifyContent: "center",
   203        flex: 1,
   204      },
   205      "& .iconContainer": {
   206        "& .min-icon": {
   207          minWidth: 140,
   208          width: "100%",
   209          maxHeight: 55,
   210          height: "100%",
   211        },
   212      },
   213    },
   214  
   215    "& .open-source": {
   216      fontSize: "14px",
   217      display: "flex",
   218      marginBottom: "5px",
   219      alignItems: "center",
   220      "& .min-icon": {
   221        marginRight: "8px",
   222        height: "12px",
   223        width: "12px",
   224      },
   225    },
   226  
   227    "& .cur-plan-text": {
   228      fontSize: "12px",
   229      textTransform: "uppercase",
   230    },
   231  
   232    "@media (max-width: 600px)": {
   233      cursor: "pointer",
   234      "& .title-block": {
   235        "& .title": {
   236          fontSize: "14px",
   237          fontWeight: 600,
   238        },
   239      },
   240    },
   241  
   242    "&.active, &.active.xs-active": {
   243      color: "#ffffff",
   244      position: "relative",
   245  
   246      "& .min-icon": {
   247        fill: "#ffffff",
   248      },
   249  
   250      "&:before": {
   251        content: "' '",
   252        position: "absolute",
   253        width: "100%",
   254        height: "18px",
   255        backgroundColor: get(theme, "signalColors.info", "#2781B0"),
   256        display: "block",
   257        top: -16,
   258      },
   259      "& .iconContainer": {
   260        "& .min-icon": {
   261          marginTop: "-12px",
   262        },
   263      },
   264    },
   265    "&.active": {
   266      backgroundColor: get(theme, "signalColors.info", "#2781B0"),
   267      color: "#ffffff",
   268    },
   269    "&.xs-active": {
   270      background: "#eaeaea",
   271    },
   272  }));
   273  
   274  const ListContainer = styled.div(({ theme }) => ({
   275    border: `1px solid ${get(theme, "borderColor", "#EAEAEA")}`,
   276    borderTop: "0px",
   277    marginBottom: "45px",
   278    "&::-webkit-scrollbar": {
   279      width: "5px",
   280      height: "5px",
   281    },
   282    "&::-webkit-scrollbar-track": {
   283      background: "#F0F0F0",
   284      borderRadius: 0,
   285      boxShadow: "inset 0px 0px 0px 0px #F0F0F0",
   286    },
   287    "&::-webkit-scrollbar-thumb": {
   288      background: "#777474",
   289      borderRadius: 0,
   290    },
   291    "&::-webkit-scrollbar-thumb:hover": {
   292      background: "#5A6375",
   293    },
   294  }));
   295  
   296  const PlanHeader = ({
   297    isActive,
   298    isXsViewActive,
   299    title,
   300    onClick,
   301    children,
   302  }: {
   303    isActive: boolean;
   304    isXsViewActive: boolean;
   305    title: string;
   306    price?: string;
   307    onClick: any;
   308    children: any;
   309  }) => {
   310    const plan = title.toLowerCase();
   311    return (
   312      <PlanHeaderContainer
   313        className={clsx({
   314          "plan-header": true,
   315          active: isActive,
   316          [`xs-active`]: isXsViewActive,
   317        })}
   318        onClick={() => {
   319          onClick && onClick(plan);
   320        }}
   321      >
   322        {children}
   323      </PlanHeaderContainer>
   324    );
   325  };
   326  
   327  const FeatureTitleRowCmp = (props: { featureLabel: any }) => {
   328    return (
   329      <Box className="feature-title">
   330        <Box className="feature-title-info">
   331          <div className="xs-only">{props.featureLabel} </div>
   332        </Box>
   333      </Box>
   334    );
   335  };
   336  
   337  const PricingFeatureItem = (props: {
   338    featureLabel: any;
   339    label?: any;
   340    detail?: any;
   341    xsLabel?: string;
   342    style?: any;
   343  }) => {
   344    return (
   345      <Box className="feature-item" style={props.style}>
   346        <Box className="feature-item-info">
   347          <div className="xs-only">
   348            {getRenderValue(props.featureLabel || "")}
   349          </div>
   350          <Box className="plan-feature">
   351            <div>{getRenderValue(props.label || "")}</div>
   352            {getRenderValue(props.detail)}
   353  
   354            <div className="xs-only">{props.xsLabel} </div>
   355          </Box>
   356        </Box>
   357      </Box>
   358    );
   359  };
   360  
   361  const LicensePlans = ({ licenseInfo }: IRegisterStatus) => {
   362    const [isSmallScreen, setIsSmallScreen] = useState<boolean>(
   363      window.innerWidth >= breakPoints.sm,
   364    );
   365  
   366    useEffect(() => {
   367      const handleWindowResize = () => {
   368        let extMD = false;
   369        if (window.innerWidth >= breakPoints.sm) {
   370          extMD = true;
   371        }
   372        setIsSmallScreen(extMD);
   373      };
   374  
   375      window.addEventListener("resize", handleWindowResize);
   376  
   377      return () => {
   378        window.removeEventListener("resize", handleWindowResize);
   379      };
   380    }, []);
   381  
   382    let currentPlan = !licenseInfo
   383      ? "community"
   384      : licenseInfo?.plan?.toLowerCase();
   385  
   386    const isCommunityPlan = currentPlan === LICENSE_PLANS.COMMUNITY;
   387    const isStandardPlan = currentPlan === LICENSE_PLANS.STANDARD;
   388    const isEnterprisePlan = currentPlan === LICENSE_PLANS.ENTERPRISE;
   389  
   390    const isPaidPlan = PAID_PLANS.includes(currentPlan);
   391  
   392    /*In smaller screen use tabbed view to show features*/
   393    const [xsPlanView, setXsPlanView] = useState("");
   394    let isXsViewCommunity = xsPlanView === LICENSE_PLANS.COMMUNITY;
   395    let isXsViewStandard = xsPlanView === LICENSE_PLANS.STANDARD;
   396    let isXsViewEnterprise = xsPlanView === LICENSE_PLANS.ENTERPRISE;
   397  
   398    const getCommunityPlanHeader = () => {
   399      return (
   400        <PlanHeader
   401          key={"community-header"}
   402          isActive={isCommunityPlan}
   403          isXsViewActive={isXsViewCommunity}
   404          title={"community"}
   405          onClick={isSmallScreen ? onPlanClick : null}
   406        >
   407          <Box className="title-block">
   408            <Box className="title-main">
   409              <div className="iconContainer">
   410                <AGPLV3Logo style={{ width: 117 }} />
   411              </div>
   412            </Box>
   413          </Box>
   414        </PlanHeader>
   415      );
   416    };
   417  
   418    const getStandardPlanHeader = () => {
   419      return (
   420        <PlanHeader
   421          key={"standard-header"}
   422          isActive={isStandardPlan}
   423          isXsViewActive={isXsViewStandard}
   424          title={"Standard"}
   425          onClick={isSmallScreen ? onPlanClick : null}
   426        >
   427          <Box className="title-block">
   428            <Box className="title-main">
   429              <div className="iconContainer">
   430                <ConsoleStandard />
   431              </div>
   432            </Box>
   433          </Box>
   434        </PlanHeader>
   435      );
   436    };
   437  
   438    const getEnterpriseHeader = () => {
   439      return (
   440        <PlanHeader
   441          key={"enterprise-header"}
   442          isActive={isEnterprisePlan}
   443          isXsViewActive={isXsViewEnterprise}
   444          title={"Enterprise"}
   445          onClick={isSmallScreen ? onPlanClick : null}
   446        >
   447          <Box className="title-block">
   448            <Box className="title-main">
   449              <div className="iconContainer">
   450                <ConsoleEnterprise />
   451              </div>
   452            </Box>
   453          </Box>
   454        </PlanHeader>
   455      );
   456    };
   457  
   458    const getButton = (
   459      link: string,
   460      btnText: string,
   461      variant: any,
   462      plan: string,
   463    ) => {
   464      let linkToNav =
   465        currentPlan !== "community" ? "https://subnet.min.io" : link;
   466      return (
   467        <Button
   468          id={`license-action-${link}`}
   469          variant={variant}
   470          style={{
   471            marginTop: "12px",
   472            width: "80%",
   473          }}
   474          disabled={
   475            currentPlan !== LICENSE_PLANS.COMMUNITY && currentPlan !== plan
   476          }
   477          onClick={(e) => {
   478            e.preventDefault();
   479  
   480            window.open(`${linkToNav}?ref=con`, "_blank");
   481          }}
   482          label={btnText}
   483        />
   484      );
   485    };
   486  
   487    const onPlanClick = (plan: string) => {
   488      setXsPlanView(plan);
   489    };
   490  
   491    useEffect(() => {
   492      if (isSmallScreen) {
   493        setXsPlanView(currentPlan || "community");
   494      } else {
   495        setXsPlanView("");
   496      }
   497    }, [isSmallScreen, currentPlan]);
   498  
   499    const featureList = FEATURE_ITEMS;
   500    return (
   501      <Fragment>
   502        <ListContainer>
   503          <Box
   504            className={"title-blue-bar"}
   505            sx={{
   506              height: "8px",
   507              borderBottom: "8px solid rgb(6 48 83)",
   508            }}
   509          />
   510          <PlanListContainer className={isPaidPlan ? "paid-plans-only" : ""}>
   511            <Box className="features-col">
   512              {featureList.map((fi) => {
   513                const featureTitleRow = fi.featureTitleRow;
   514                const isHeader = fi.isHeader;
   515  
   516                if (isHeader) {
   517                  if (isPaidPlan) {
   518                    return (
   519                      <Box
   520                        key={`plan-header-${fi.desc}`}
   521                        className="plan-header"
   522                        sx={{
   523                          fontSize: "14px",
   524                          paddingLeft: "26px",
   525                          display: "flex",
   526                          alignItems: "center",
   527                          justifyContent: "flex-start",
   528                          borderBottom: "0px !important",
   529  
   530                          "& .link-text": {
   531                            color: "#2781B0",
   532                            cursor: "pointer",
   533                            textDecoration: "underline",
   534                          },
   535  
   536                          "& .min-icon": {
   537                            marginRight: "10px",
   538                            color: "#2781B0",
   539                            fill: "#2781B0",
   540                          },
   541                        }}
   542                      >
   543                        <LicenseDocIcon />
   544                        <a
   545                          href={`https://subnet.min.io/terms-and-conditions/${currentPlan}`}
   546                          rel="noopener"
   547                          className={"link-text"}
   548                        >
   549                          View License agreement <br />
   550                          for the registered plan.
   551                        </a>
   552                      </Box>
   553                    );
   554                  }
   555  
   556                  return (
   557                    <Box
   558                      key={`plan-header-label-${fi.desc}`}
   559                      className={`plan-header`}
   560                      sx={{
   561                        fontSize: "14px",
   562                        paddingLeft: "26px",
   563                        display: "flex",
   564                        alignItems: "center",
   565                        justifyContent: "flex-start",
   566                        borderBottom: "0px !important",
   567                      }}
   568                    >
   569                      {fi.label}
   570                    </Box>
   571                  );
   572                }
   573                if (featureTitleRow) {
   574                  return (
   575                    <Box
   576                      key={`plan-descript-${fi.desc}`}
   577                      className="feature-title"
   578                      sx={{
   579                        fontSize: "14px",
   580                        fontWeight: 600,
   581                        textTransform: "uppercase",
   582                      }}
   583                    >
   584                      <div>{getRenderValue(fi.desc)} </div>
   585                    </Box>
   586                  );
   587                }
   588                return (
   589                  <Box
   590                    key={`plan-feature-name-${fi.desc}`}
   591                    className="feature-name"
   592                    style={fi.style}
   593                  >
   594                    <div>{getRenderValue(fi.desc)} </div>
   595                  </Box>
   596                );
   597              })}
   598            </Box>
   599            {!isPaidPlan ? (
   600              <Box
   601                className={`plan-col ${
   602                  isCommunityPlan ? "active-plan-col" : "non-active-plan-col"
   603                }`}
   604              >
   605                {COMMUNITY_PLAN_FEATURES.map((fi, idx) => {
   606                  const featureLabel = featureList[idx].desc;
   607                  const { featureTitleRow, isHeader } = fi;
   608  
   609                  if (isHeader) {
   610                    return getCommunityPlanHeader();
   611                  }
   612  
   613                  if (featureTitleRow) {
   614                    return (
   615                      <FeatureTitleRowCmp
   616                        key={`title-row-${fi.id}`}
   617                        featureLabel={featureLabel}
   618                      />
   619                    );
   620                  }
   621  
   622                  return (
   623                    <PricingFeatureItem
   624                      key={`pricing-feature-${fi.id}`}
   625                      featureLabel={featureLabel}
   626                      label={fi.label}
   627                      detail={fi.detail}
   628                      xsLabel={fi.xsLabel}
   629                      style={fi.style}
   630                    />
   631                  );
   632                })}
   633                <Box className="button-box">
   634                  {getButton(
   635                    `https://slack.min.io`,
   636                    "Join Slack",
   637                    "regular",
   638                    LICENSE_PLANS.COMMUNITY,
   639                  )}
   640                </Box>
   641              </Box>
   642            ) : null}
   643            <Box
   644              className={`plan-col ${
   645                isStandardPlan ? "active-plan-col" : "non-active-plan-col"
   646              }`}
   647            >
   648              {STANDARD_PLAN_FEATURES.map((fi, idx) => {
   649                const featureLabel = featureList[idx].desc;
   650                const featureTitleRow = fi.featureTitleRow;
   651                const isHeader = fi.isHeader;
   652  
   653                if (isHeader) {
   654                  return getStandardPlanHeader();
   655                }
   656  
   657                if (featureTitleRow) {
   658                  return (
   659                    <FeatureTitleRowCmp
   660                      key={`feature-title-row-${fi.id}`}
   661                      featureLabel={featureLabel}
   662                    />
   663                  );
   664                }
   665                return (
   666                  <PricingFeatureItem
   667                    key={`feature-item-${fi.id}`}
   668                    featureLabel={featureLabel}
   669                    label={fi.label}
   670                    detail={fi.detail}
   671                    xsLabel={fi.xsLabel}
   672                    style={fi.style}
   673                  />
   674                );
   675              })}
   676  
   677              <Box className="button-box">
   678                {getButton(
   679                  `https://min.io/signup`,
   680                  !PAID_PLANS.includes(currentPlan)
   681                    ? "Subscribe"
   682                    : "Login to SUBNET",
   683                  "callAction",
   684                  LICENSE_PLANS.STANDARD,
   685                )}
   686              </Box>
   687            </Box>
   688            <Box
   689              className={`plan-col ${
   690                isEnterprisePlan ? "active-plan-col" : "non-active-plan-col"
   691              }`}
   692            >
   693              {ENTERPRISE_PLAN_FEATURES.map((fi, idx) => {
   694                const featureLabel = featureList[idx].desc;
   695                const { featureTitleRow, isHeader, yesIcon } = fi;
   696  
   697                if (isHeader) {
   698                  return getEnterpriseHeader();
   699                }
   700  
   701                if (featureTitleRow) {
   702                  return (
   703                    <FeatureTitleRowCmp
   704                      key={`feature-title-row2-${fi.id}`}
   705                      featureLabel={featureLabel}
   706                    />
   707                  );
   708                }
   709  
   710                if (yesIcon) {
   711                  return (
   712                    <Box className="feature-item" key={`ent-feature-yes${fi.id}`}>
   713                      <Box className="feature-item-info">
   714                        <div className="xs-only"></div>
   715                        <Box className="plan-feature">
   716                          <CheckCircleIcon />
   717                        </Box>
   718                      </Box>
   719                    </Box>
   720                  );
   721                }
   722                return (
   723                  <PricingFeatureItem
   724                    key={`pricing-feature-item-${fi.id}`}
   725                    featureLabel={featureLabel}
   726                    label={fi.label}
   727                    detail={fi.detail}
   728                    style={fi.style}
   729                  />
   730                );
   731              })}
   732              <Box className="button-box">
   733                {getButton(
   734                  `https://min.io/signup`,
   735                  !PAID_PLANS.includes(currentPlan)
   736                    ? "Subscribe"
   737                    : "Login to SUBNET",
   738                  "callAction",
   739                  LICENSE_PLANS.ENTERPRISE,
   740                )}
   741              </Box>
   742            </Box>
   743          </PlanListContainer>
   744        </ListContainer>
   745      </Fragment>
   746    );
   747  };
   748  
   749  export default LicensePlans;