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;