From d19fefab71bebc0c41b34c0696c4dfed50cf9f33 Mon Sep 17 00:00:00 2001 From: Frank Macreery Date: Tue, 15 Oct 2024 18:38:00 -0400 Subject: [PATCH 1/3] Update plans to Development/Production/Enterprise without deploy-api code changes --- src/app/test/no-payment-found.test.tsx | 8 +- src/deploy/plan/index.ts | 2 + src/mocks/data.ts | 2 +- src/schema/factory.ts | 2 +- src/types/deploy.ts | 9 +- src/ui/pages/create-stack.tsx | 11 +- src/ui/pages/plans.test.tsx | 8 +- src/ui/shared/plan.tsx | 168 ++++++++++++------------- 8 files changed, 110 insertions(+), 100 deletions(-) diff --git a/src/app/test/no-payment-found.test.tsx b/src/app/test/no-payment-found.test.tsx index f3912372a..998e6c456 100644 --- a/src/app/test/no-payment-found.test.tsx +++ b/src/app/test/no-payment-found.test.tsx @@ -71,7 +71,7 @@ describe("Payment page takeover", () => { { ...testPlan, id: fixedIdForTests, - name: "growth", + name: "production", }, testEnterprisePlan, ], @@ -90,13 +90,13 @@ describe("Payment page takeover", () => { render(); await screen.findByText(/Choose a Plan/); - await screen.findByText(/Growth/); + await screen.findByText(/Development/); const el = await screen.findAllByRole("button", { name: /Select Plan/, }); fireEvent.click(el[0]); - await screen.findByText(/Successfully updated plan to Growth/); + await screen.findByText(/Successfully updated plan to Development/); }); }); @@ -162,7 +162,7 @@ describe("Payment page takeover", () => { { ...testPlan, id: fixedIdForTests, - name: "growth", + name: "production", }, testEnterprisePlan, ], diff --git a/src/deploy/plan/index.ts b/src/deploy/plan/index.ts index aff819272..f2f5a9407 100644 --- a/src/deploy/plan/index.ts +++ b/src/deploy/plan/index.ts @@ -198,8 +198,10 @@ export const selectPlansForView = createSelector( const init: Record = { none: defaultPlan({ name: "none" }), starter: defaultPlan({ name: "starter" }), + development: defaultPlan({ name: "development" }), growth: defaultPlan({ name: "growth" }), scale: defaultPlan({ name: "scale" }), + production: defaultPlan({ name: "production" }), enterprise: defaultPlan({ name: "enterprise" }), }; return plans.reduce((acc, plan) => { diff --git a/src/mocks/data.ts b/src/mocks/data.ts index 432899f82..dc9b9d522 100644 --- a/src/mocks/data.ts +++ b/src/mocks/data.ts @@ -502,7 +502,7 @@ export const testEnterprisePlan = defaultPlanResponse({ export const testActivePlan = defaultActivePlanResponse({ id: createId(), - available_plans: ["growth", "scale"], + available_plans: ["development", "production"], organization_id: testOrg.id, _links: { organization: defaultHalHref( diff --git a/src/schema/factory.ts b/src/schema/factory.ts index 9318abc4d..8104e3a7d 100644 --- a/src/schema/factory.ts +++ b/src/schema/factory.ts @@ -488,7 +488,7 @@ export const defaultActivePlan = ( return { id: "", automatedBackupLimitPerDb: 0, - availablePlans: ["starter", "growth", "scale"], + availablePlans: ["starter", "development", "growth", "scale", "production"], complianceDashboardAccess: false, containerMemoryLimit: 0, costCents: 0, diff --git a/src/types/deploy.ts b/src/types/deploy.ts index e3491f588..13b3c1717 100644 --- a/src/types/deploy.ts +++ b/src/types/deploy.ts @@ -406,7 +406,14 @@ export interface DeployPrereleaseCommand extends Timestamps { index: number; } -export type PlanName = "starter" | "growth" | "scale" | "enterprise" | "none"; +export type PlanName = + | "starter" + | "development" + | "growth" + | "scale" + | "production" + | "enterprise" + | "none"; export interface DeployPlan extends Timestamps { id: string; automatedBackupLimitPerDb: number; diff --git a/src/ui/pages/create-stack.tsx b/src/ui/pages/create-stack.tsx index bb51341d0..a4dc1ff23 100644 --- a/src/ui/pages/create-stack.tsx +++ b/src/ui/pages/create-stack.tsx @@ -87,9 +87,12 @@ export const CreateStackPage = () => { ); // must have a non-starter active plan or, // an empty active plan (empty active plan means legacy enterprise) - const canRequestStack = ["growth", "scale", "enterprise"].includes( - selectedPlan.name, - ); + const canRequestStack = [ + "growth", + "scale", + "production", + "enterprise", + ].includes(selectedPlan.name); const [stackName, setStackName] = useState(""); const [region, setRegion] = useState("none"); @@ -193,7 +196,7 @@ export const CreateStackPage = () => { {selectedPlan.id === "" || canRequestStack ? null : ( Dedicated stacks are only available for{" "} - Growth, Scale, and Enterprise plans —{" "} + Production and Enterprise plans —{" "} Upgrade your plan for access. )} diff --git a/src/ui/pages/plans.test.tsx b/src/ui/pages/plans.test.tsx index 388c15885..6acee89eb 100644 --- a/src/ui/pages/plans.test.tsx +++ b/src/ui/pages/plans.test.tsx @@ -62,7 +62,7 @@ const setupActionablePlanResponses = (extra: RestHandler[] = []) => { { ...testPlan, id: fixedIdForTests, - name: "growth", + name: "development", }, testEnterprisePlan, ], @@ -100,7 +100,7 @@ describe("Plans page", () => { ); setupActionablePlanResponses(); await screen.findByText("Choose a Plan"); - await screen.findByText("Growth"); + await screen.findByText("Development"); const errText = screen.queryByText( "Unable to load plan data to allow for selection.", @@ -112,7 +112,7 @@ describe("Plans page", () => { }); fireEvent.click(el[0]); - await screen.findByText(/Successfully updated plan to Growth/); + await screen.findByText(/Successfully updated plan to Development/); }); it("the plans page is visible, renders with plans found, but errors when user selects", async () => { @@ -127,7 +127,7 @@ describe("Plans page", () => { ); await screen.findByText("Choose a Plan"); - await screen.findByText("Growth"); + await screen.findByText("Development"); const el = await screen.findAllByRole("button", { name: /Select Plan/, diff --git a/src/ui/shared/plan.tsx b/src/ui/shared/plan.tsx index 48e277688..8f288d657 100644 --- a/src/ui/shared/plan.tsx +++ b/src/ui/shared/plan.tsx @@ -9,11 +9,17 @@ import { tokens } from "./tokens"; const descriptionTextForPlan = (planName: PlanName): string => ({ none: "", - starter: "Deploy your app and get to market fast", + // Legacy + starter: "Get started in a limited trial environment", growth: "Gain user traction and deliver more functionality", scale: "Run mission-critical apps at scale without worry", + + // Current + development: "Develop your prototype with a seamless path to production", + production: + "Go to production in your own private network with everything you need to meet compliance requirements", enterprise: - "Meet any requirement across performance, reliability, and security", + "Leverage Aptible at scale with enterprise features and support for additional compliance frameworks", })[planName]; const Section = ({ @@ -79,10 +85,8 @@ const PlanButton = ({ const PlanCostBlock = ({ price, - max, }: { price: string; - max: string; }) => { return (
@@ -90,10 +94,6 @@ const PlanCostBlock = ({

Starts at

{price}

-
-

Approx. Max Invoice

-

{max}

-
); }; @@ -123,7 +123,7 @@ const BulletListForPlan = ({ if (plan.name === "starter") { return ( <> - +
Includes @@ -159,7 +159,7 @@ const BulletListForPlan = ({ if (plan.name === "growth") { return ( <> - +
Includes @@ -198,7 +198,7 @@ const BulletListForPlan = ({ if (plan.name === "scale") { return ( <> - +
Includes @@ -226,51 +226,57 @@ const BulletListForPlan = ({ Up to 3 concurrent SSH Sessions for temporary container access Support: Choose from Standard or Premium - Dedicated Stacks (Isolated Tenancy) Available + Dedicated Stacks (Isolated Tenancy) available Available HIPAA BAA ); } - return ( - <> - + if (plan.name === "development") { + return ( + <> + -
- Includes - Available -
+
    + + No limits on available Compute, Database Storage, or Endpoints + + Standard Support +
+ + ); + } -
- Custom - Unlimited -
+ if (plan.name === "production") { + return ( + <> + -
- Custom - Unlimited -
+
    + Deploy in 15+ regions + Custom Domains for Apps + Dedicated Stacks (isolated tenancy) available + Available HIPAA BAA + Support: Choose from Standard or Premium +
+ + ); + } -
- Custom - Unlimited -
+ return ( + <> +
    - - No limits on available Compute, Database Storage, or Endpoints - - Deploy in 15+ Regions 99.95% Uptime SLA + Advanced networking features like IPsec VPNs + Option to self-host in your AWS - Advanced Networking Features such as IPsec VPNs and VPC Peering - - - Available HITRUST Inheritance and Security & Compliance Dashboard + Available HITRUST inheritance and security & compliance consulting - Support: Choose from Standard, Premium, Enterprise (24/7 Support) + Support: Choose from Standard, Premium, Enterprise (24/7 response) Custom pricing and payment options with annual commitments and @@ -349,53 +355,45 @@ export const Plans = ({ paymentRequired: boolean; }) => { const plans = useSelector(selectPlansForView); + const publishedPlans = [ + plans.development, + plans.production, + plans.enterprise, + ]; + + // Only show other plans (starter, growth, scale) if they are selected + let plansToShow: DeployPlan[] = publishedPlans; + if (selected === "starter") { + plansToShow = [plans.starter].concat(publishedPlans); + } else if (selected === "growth") { + plansToShow = [plans.growth].concat(publishedPlans); + } else if (selected === "scale") { + plansToShow = [plans.scale].concat(publishedPlans); + } + + // Show 3 column layout if only 3 plans to show, otherwise 4 (when there should be 4 to show) + let col = "lg:grid-cols-3"; + if (plansToShow.length > 3) { + col = "lg:grid-cols-4"; + } return ( -
    - - onSelectPlan({ planId: plans.starter.id, name: plans.starter.name }) - } - /> - - - onSelectPlan({ planId: plans.growth.id, name: plans.growth.name }) - } - /> - - - onSelectPlan({ planId: plans.scale.id, name: plans.scale.name }) - } - /> - - - onSelectPlan({ - planId: plans.enterprise.id, - name: plans.enterprise.name, - }) - } - /> +
    + {plansToShow.map((plan, index) => ( + + onSelectPlan({ planId: plan.id, name: plan.name }) + } + /> + ))}
    ); }; From 75d62efa8db4e12fb4d91909a92c02f45302a4ad Mon Sep 17 00:00:00 2001 From: Frank Macreery Date: Wed, 16 Oct 2024 09:34:00 -0400 Subject: [PATCH 2/3] Small cosmetic change to tests (does not change test success) Updated mock plan `name` to match descriptive text --- src/app/test/no-payment-found.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/test/no-payment-found.test.tsx b/src/app/test/no-payment-found.test.tsx index 998e6c456..e70cb2674 100644 --- a/src/app/test/no-payment-found.test.tsx +++ b/src/app/test/no-payment-found.test.tsx @@ -71,7 +71,7 @@ describe("Payment page takeover", () => { { ...testPlan, id: fixedIdForTests, - name: "production", + name: "development", }, testEnterprisePlan, ], @@ -162,7 +162,7 @@ describe("Payment page takeover", () => { { ...testPlan, id: fixedIdForTests, - name: "production", + name: "development", }, testEnterprisePlan, ], From f6d73c0dc086ab81ba6e9016538e77c7dc777bfa Mon Sep 17 00:00:00 2001 From: Frank Macreery Date: Wed, 16 Oct 2024 11:37:16 -0400 Subject: [PATCH 3/3] Add loading state to /plans buttons --- src/ui/shared/plan.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/shared/plan.tsx b/src/ui/shared/plan.tsx index 8f288d657..b9bc1bf2b 100644 --- a/src/ui/shared/plan.tsx +++ b/src/ui/shared/plan.tsx @@ -1,5 +1,5 @@ -import { selectPlansForView } from "@app/deploy"; -import { useSelector } from "@app/react"; +import { selectPlansForView, updateActivePlan } from "@app/deploy"; +import { useLoader, useSelector } from "@app/react"; import { capitalize } from "@app/string-utils"; import type { DeployActivePlan, DeployPlan, PlanName } from "@app/types"; import { Button, ButtonLinkExternal } from "./button"; @@ -52,6 +52,7 @@ const PlanButton = ({ onClick: (e: React.MouseEvent) => void; selected: boolean; }) => { + const loader = useLoader(updateActivePlan); if (contactUs) { return ( @@ -77,7 +78,7 @@ const PlanButton = ({ } return ( - ); @@ -381,7 +382,7 @@ export const Plans = ({
    - {plansToShow.map((plan, index) => ( + {plansToShow.map((plan) => (