From 72f3a4b1282124e132cd6375ededee89f206accc Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 8 Jul 2024 14:42:19 +0100 Subject: [PATCH] Usage and billing (v3) (#1201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v3 subscription endpoints * Use pnpm linked billing package during development * Moved v2 billing components into a subfolder * Select plan using the real data * Improved v3 plan display * Use new api response that doesn’t require a Stripe call * Free flow is working * Added GitHub modal and verified badge * Deleted old request v3 access component/route * Allow setting classes on the Tooltip button * Paid plans working * Redirect from select plan if you’ve got v3 enabled * Loading state improvements * New admin API endpoint to set concurrency across multiple environments * When projects are created, conditionally create staging based on the plan * New billing page working with side menu and stripe portal * Layout, formatting and some plan state improvements/fixes * Don’t show the period if you’re on the free plan * Temporary upgrade callout * Refactored the platform code so it’s easier to call and doesn’t require a isManagedCloud check * Send taskIdentifier to OpenMeter * Side menu * Upgrade prompts * More improvements to the app-wide usage indicators * Early work on usage graphs * Added the usage bar for v3 * Moved code to presenter and now using defer * Added the tasks table to usage * Improved the v3 usage bar if theres’ no usage on a paid plan * If no run data, still render a graph * Usage page errors when defered loading fails * Don’t show the public API key for v3, they’re not used and probably never will be * Improved the upgrade callout and API keys page layout * Only show the “reveal all” toggle if you have environment variables in the table * Replaced Upgrade callout with a more generic InfoPanel component * better panel width * Show conditional upgrade prompts based on plan and number of schedules used * Removed duplicate class * Wider blank state panels for the scheduled page * Wider info panel for the env var page * Blank state now using the info panel * Platform alerts prompt now using the InfoPanel * Deploy blank state uses InfoPanel * Github verified badge padding adjustment * Improved the layout of the page, some style tweaks, organized imports * Better default tooltip style * could be undefined fix * text fix + style updates * Changed the billing icon in the side menu * Billing page layout and style improvements * plan tooltips don’t use dark variant * Don’t highlight the plan on the billing page * Tooltip underlines stand out more * Fixed padding in the PageTitle * Tooltips use the correct cursor * Improved the plan banner on the billing page * Fixed Header1 inconsistent font weight * Fixed issue where input field focus states were being clipped * Fixed large button not having large text size * Added a link to the Get in touch copy and improved the connect to GitHub modal * Select plan page uses the MainCenteredContainer * Better logging from the Loops endpoint because this error finally got hit * Move the ingestion of compute to the platform * Reporting usage of invocations moved to the platform * Get the entitlement before triggering a non-dev task * Contact us enterprise plan button opens the feedback form * Removed Github discussions link from the Feedback panel * Swapped billing icon for credit card * Show a Unlock staging panel on the env var page * Updated staging environment colour * Show a prompt to upgrade to get staging in the new env var modal * Improved the edit env var modal * Implement ability to disable org concurrency * Use common logic for the plans * Use the billing server to get the schedule limits * Some schedules page fixes * More convenient way of getting a limit * Use the new schedule limit * Team member limiting * Made the limit visible on the team page * Limit alerts * Added an index for TaskRun.scheduleId * Remove console.log on schedules page * Added durations to the run table * Tabular numbers * Improved the usage page formatting * Only admins see the compute column on the run table * Include the base cost on the usage stats * Moved the status to the sidebar * Optional table header tooltip * Allow InfoIconTooltips to have customizable content styles * Added a tooltip to the duration header, changed no test to a dash * Removed all references to signing up to v3 from the docs * Switched @trigger.dev/billing to @trigger.dev/platform * Passing up the variant for the InfoIconTooltip * table tooltip max-width fixed * Switch to the published @trigger.dev/platform 1.0.11 * v2 usage page title changed to include “v2" * code theme has a transparent background so it works on any background * duration columns now grouped together nicely at wide screen size * Last duration column fills the width properly * Fix for the per run price being in cents not dollars * Show the total cost with 8 decimal places * Show 8 decimal places in the usage graph tooltip * Moved the UpgradePrompt to the v3 folder * Prepare to use Shadcns chart helpers * Much nicer chart * Small tweaks to the graph * Fix run table col spans for empty/loading messages * We don’t need isManagedCloud in createProject * Hide v3 usage/billing pages if there aren’t v3 projects in your org * Removed unused tooltipStyle * Usage bar now says “Included usage” instead of “Tier limit” if you’re paying * Get the plan/usage data in parallel * The usage page now has a month dropdown and all data is for that calendar month * Ensure the passed date is the 1st of the month * Use the machine presets from the platform package --------- Co-authored-by: James Ritchie Co-authored-by: Eric Allam --- .../app/components/DefinitionTooltip.tsx | 4 +- apps/webapp/app/components/Feedback.tsx | 54 +- .../app/components/billing/UpgradePrompt.tsx | 61 -- .../billing/{ => v2}/ConcurrentRunsChart.tsx | 2 +- .../billing/{ => v2}/DailyRunsChat.tsx | 2 +- .../billing/{ => v2}/FreePlanUsage.tsx | 4 +- .../billing/{ => v2}/PricingCalculator.tsx | 8 +- .../billing/{ => v2}/PricingTiers.tsx | 12 +- .../{ => v2}/RunsVolumeDiscountTable.tsx | 6 +- .../components/billing/{ => v2}/UsageBar.tsx | 4 +- .../components/billing/v3/UpgradePrompt.tsx | 53 ++ .../app/components/billing/v3/UsageBar.tsx | 170 ++++ apps/webapp/app/components/code/CodeBlock.tsx | 2 +- .../app/components/layout/AppLayout.tsx | 6 +- .../app/components/navigation/SideMenu.tsx | 167 ++-- .../app/components/primitives/Buttons.tsx | 2 +- .../app/components/primitives/Chart.tsx | 324 ++++++++ .../app/components/primitives/Headers.tsx | 2 +- .../app/components/primitives/InfoPanel.tsx | 71 ++ .../app/components/primitives/PageHeader.tsx | 17 +- .../app/components/primitives/Table.tsx | 15 +- .../app/components/primitives/Tooltip.tsx | 15 +- .../app/components/runs/v3/TaskRunsTable.tsx | 135 +++- apps/webapp/app/env.server.ts | 11 +- apps/webapp/app/models/member.server.ts | 10 +- apps/webapp/app/models/organization.server.ts | 4 +- apps/webapp/app/models/project.server.ts | 21 +- .../app/presenters/OrgBillingPlanPresenter.ts | 2 +- .../presenters/OrgUsagePresenter.server.ts | 4 +- .../app/presenters/TeamPresenter.server.ts | 26 + .../v3/AlertChannelListPresenter.server.ts | 20 + .../EnvironmentVariablesPresenter.server.ts | 1 + .../presenters/v3/RunListPresenter.server.ts | 8 +- .../v3/ScheduleListPresenter.server.ts | 14 +- .../presenters/v3/UsagePresenter.server.ts | 115 +++ .../route.tsx | 32 + .../route.tsx | 46 ++ .../route.tsx | 34 + .../route.tsx | 44 ++ .../route.tsx | 11 +- .../route.tsx | 6 +- .../route.tsx | 21 +- .../route.tsx | 64 +- .../route.tsx | 61 +- .../route.tsx | 214 ++--- .../route.tsx | 30 +- .../route.tsx | 104 ++- .../route.tsx | 67 +- .../route.tsx | 25 +- .../route.tsx | 132 ++-- .../route.tsx | 81 +- .../route.tsx | 143 ++++ .../route.tsx | 329 ++++++++ .../_app.orgs.$organizationSlug/route.tsx | 25 +- .../route.tsx | 54 +- .../route.tsx | 93 +-- .../route.tsx | 19 +- ...api.v1.orgs.$organizationId.concurrency.ts | 104 +++ apps/webapp/app/routes/api.v1.usage.ingest.ts | 92 +-- .../resources.$organizationSlug.subscribe.ts | 4 +- ...s.$organizationSlug.subscription.portal.ts | 2 +- ...organizationSlug.subscription.v3.portal.ts | 45 ++ ...ces.orgs.$organizationSlug.select-plan.tsx | 731 ++++++++++++++++++ ...urces.orgs.$organizationSlug.v3-access.tsx | 210 ----- .../storybook.free-plan-usage/route.tsx | 2 +- .../app/routes/storybook.usage/route.tsx | 2 +- ...billing.server.ts => billing.v2.server.ts} | 8 +- .../app/services/deleteOrganization.server.ts | 2 +- apps/webapp/app/services/loops.server.ts | 9 +- .../webapp/app/services/platform.v3.server.ts | 307 ++++++++ apps/webapp/app/services/worker.server.ts | 16 +- apps/webapp/app/tailwind.css | 17 + apps/webapp/app/utils/graphs.ts | 48 ++ apps/webapp/app/utils/numberFormatter.ts | 12 + apps/webapp/app/utils/pathBuilder.ts | 18 +- apps/webapp/app/v3/machinePresets.server.ts | 48 +- apps/webapp/app/v3/marqs/index.server.ts | 93 ++- .../app/v3/marqs/marqsKeyProducer.server.ts | 9 +- .../v3/marqs/sharedQueueConsumer.server.ts | 2 +- apps/webapp/app/v3/marqs/types.ts | 1 + apps/webapp/app/v3/openMeter.server.ts | 47 -- .../app/v3/services/triggerTask.server.ts | 14 + .../v3/services/upsertTaskSchedule.server.ts | 24 +- apps/webapp/package.json | 4 +- apps/webapp/remix.config.js | 2 +- apps/webapp/tailwind.config.js | 2 +- docs/v3/introduction.mdx | 5 - docs/v3/migration-defer.mdx | 14 +- docs/v3/quick-start.mdx | 5 - .../migration.sql | 2 + packages/database/prisma/schema.prisma | 3 + pnpm-lock.yaml | 18 +- 92 files changed, 3722 insertions(+), 1207 deletions(-) delete mode 100644 apps/webapp/app/components/billing/UpgradePrompt.tsx rename apps/webapp/app/components/billing/{ => v2}/ConcurrentRunsChart.tsx (98%) rename apps/webapp/app/components/billing/{ => v2}/DailyRunsChat.tsx (97%) rename apps/webapp/app/components/billing/{ => v2}/FreePlanUsage.tsx (95%) rename apps/webapp/app/components/billing/{ => v2}/PricingCalculator.tsx (97%) rename apps/webapp/app/components/billing/{ => v2}/PricingTiers.tsx (98%) rename apps/webapp/app/components/billing/{ => v2}/RunsVolumeDiscountTable.tsx (90%) rename apps/webapp/app/components/billing/{ => v2}/UsageBar.tsx (98%) create mode 100644 apps/webapp/app/components/billing/v3/UpgradePrompt.tsx create mode 100644 apps/webapp/app/components/billing/v3/UsageBar.tsx create mode 100644 apps/webapp/app/components/primitives/Chart.tsx create mode 100644 apps/webapp/app/components/primitives/InfoPanel.tsx create mode 100644 apps/webapp/app/presenters/TeamPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/UsagePresenter.server.ts create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.canceled/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.complete/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.failed/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx create mode 100644 apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts create mode 100644 apps/webapp/app/routes/resources.$organizationSlug.subscription.v3.portal.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx delete mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.v3-access.tsx rename apps/webapp/app/services/{billing.server.ts => billing.v2.server.ts} (94%) create mode 100644 apps/webapp/app/services/platform.v3.server.ts create mode 100644 apps/webapp/app/utils/graphs.ts delete mode 100644 apps/webapp/app/v3/openMeter.server.ts create mode 100644 packages/database/prisma/migrations/20240704205712_add_schedule_id_index_to_task_run/migration.sql diff --git a/apps/webapp/app/components/DefinitionTooltip.tsx b/apps/webapp/app/components/DefinitionTooltip.tsx index d69924149..0e2d4d436 100644 --- a/apps/webapp/app/components/DefinitionTooltip.tsx +++ b/apps/webapp/app/components/DefinitionTooltip.tsx @@ -15,11 +15,11 @@ export function DefinitionTip({ - + {children} - + {title} {typeof content === "string" ? ( {content} diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index 6caec8308..158b499d6 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -55,16 +55,6 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) { {button} - Get help from the community - - The quickest way to get help and feedback or to provide advice to others is to join our - Discord. - -
- - -
-
Send us an email We read every message and respond quickly.
@@ -106,6 +96,8 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {

+ +
Troubleshooting If you're having trouble, check out our documentation or the Trigger.dev Status page. @@ -134,44 +126,20 @@ function DiscordBanner({ className }: { className?: string }) { href="https://trigger.dev/discord" target="_blank" className={cn( - "group mb-4 flex w-full items-center justify-between rounded-md border border-charcoal-600 p-4 transition hover:border-text-link", + "group mb-4 flex w-full items-center justify-between rounded-md border border-grid-bright bg-charcoal-750 p-4 pl-6 transition hover:border-text-link", className )} > -
- - - Join our Discord community - - - Get help or answer questions from the Trigger.dev community. - +
+ +
+ + Join our Discord community + + The quickest way to get answers from the Trigger.dev community. +
); } - -function GitHubDiscussionsBanner({ className }: { className?: string }) { - return ( - -
- - - View our GitHub Discussions - - - Post your questions, feedback, and feature requests on GitHub. - -
- -
- ); -} diff --git a/apps/webapp/app/components/billing/UpgradePrompt.tsx b/apps/webapp/app/components/billing/UpgradePrompt.tsx deleted file mode 100644 index fc9a41a27..000000000 --- a/apps/webapp/app/components/billing/UpgradePrompt.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; -import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; -import tileBgPath from "~/assets/images/error-banner-tile@2x.png"; -import { MatchedOrganization } from "~/hooks/useOrganizations"; -import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; -import { formatNumberCompact } from "~/utils/numberFormatter"; -import { LinkButton } from "../primitives/Buttons"; -import { Icon } from "../primitives/Icon"; -import { Paragraph } from "../primitives/Paragraph"; - -type UpgradePromptProps = { - runsEnabled: boolean; - runCountCap: number; - planPath: string; -}; - -export function UpgradePrompt({ runsEnabled, runCountCap, planPath }: UpgradePromptProps) { - return ( -
-
- - - {runsEnabled - ? `You have exceeded the monthly ${formatNumberCompact(runCountCap)} runs - limit` - : `No runs are executing because you have exceeded the free limit`} - -
- - Upgrade - -
- ); -} - -export function useShowUpgradePrompt(organization?: MatchedOrganization) { - const currentPlan = useCurrentPlan(); - const shouldShow = - organization !== undefined && - currentPlan !== undefined && - currentPlan.usage.exceededRunCount && - currentPlan.usage.runCountCap !== undefined; - - if (!shouldShow) { - return { shouldShow }; - } - - return { - shouldShow, - runCountCap: currentPlan.usage.runCountCap!, - runsEnabled: organization.runsEnabled, - }; -} diff --git a/apps/webapp/app/components/billing/ConcurrentRunsChart.tsx b/apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx similarity index 98% rename from apps/webapp/app/components/billing/ConcurrentRunsChart.tsx rename to apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx index 5e1ef198e..88efe200a 100644 --- a/apps/webapp/app/components/billing/ConcurrentRunsChart.tsx +++ b/apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx @@ -8,7 +8,7 @@ import { XAxis, YAxis, } from "recharts"; -import { Paragraph } from "../primitives/Paragraph"; +import { Paragraph } from "../../primitives/Paragraph"; const tooltipStyle = { display: "flex", diff --git a/apps/webapp/app/components/billing/DailyRunsChat.tsx b/apps/webapp/app/components/billing/v2/DailyRunsChat.tsx similarity index 97% rename from apps/webapp/app/components/billing/DailyRunsChat.tsx rename to apps/webapp/app/components/billing/v2/DailyRunsChat.tsx index 250f84e1e..ddc36785b 100644 --- a/apps/webapp/app/components/billing/DailyRunsChat.tsx +++ b/apps/webapp/app/components/billing/v2/DailyRunsChat.tsx @@ -1,5 +1,5 @@ import { Label, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { Paragraph } from "../primitives/Paragraph"; +import { Paragraph } from "../../primitives/Paragraph"; const tooltipStyle = { display: "flex", diff --git a/apps/webapp/app/components/billing/FreePlanUsage.tsx b/apps/webapp/app/components/billing/v2/FreePlanUsage.tsx similarity index 95% rename from apps/webapp/app/components/billing/FreePlanUsage.tsx rename to apps/webapp/app/components/billing/v2/FreePlanUsage.tsx index 6a4f66b5f..729d55d8b 100644 --- a/apps/webapp/app/components/billing/FreePlanUsage.tsx +++ b/apps/webapp/app/components/billing/v2/FreePlanUsage.tsx @@ -1,6 +1,6 @@ import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { motion, useMotionValue, useTransform } from "framer-motion"; -import { Paragraph } from "../primitives/Paragraph"; +import { Paragraph } from "../../primitives/Paragraph"; import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; @@ -28,7 +28,7 @@ export function FreePlanUsage({ to, percentage }: { to: string; percentage: numb Free Plan
- Learn more + Upgrade
diff --git a/apps/webapp/app/components/billing/PricingCalculator.tsx b/apps/webapp/app/components/billing/v2/PricingCalculator.tsx similarity index 97% rename from apps/webapp/app/components/billing/PricingCalculator.tsx rename to apps/webapp/app/components/billing/v2/PricingCalculator.tsx index daca65a2e..7ec7bc8cd 100644 --- a/apps/webapp/app/components/billing/PricingCalculator.tsx +++ b/apps/webapp/app/components/billing/v2/PricingCalculator.tsx @@ -1,9 +1,9 @@ import * as Slider from "@radix-ui/react-slider"; -import { Plans, estimate } from "@trigger.dev/billing"; +import { Plans, estimate } from "@trigger.dev/platform/v2"; import { useCallback, useState } from "react"; -import { DefinitionTip } from "../DefinitionTooltip"; -import { Header2 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; +import { DefinitionTip } from "../../DefinitionTooltip"; +import { Header2 } from "../../primitives/Headers"; +import { Paragraph } from "../../primitives/Paragraph"; import { formatCurrency, formatNumberCompact } from "~/utils/numberFormatter"; import { cn } from "~/utils/cn"; diff --git a/apps/webapp/app/components/billing/PricingTiers.tsx b/apps/webapp/app/components/billing/v2/PricingTiers.tsx similarity index 98% rename from apps/webapp/app/components/billing/PricingTiers.tsx rename to apps/webapp/app/components/billing/v2/PricingTiers.tsx index dfe216f43..54e4aedd6 100644 --- a/apps/webapp/app/components/billing/PricingTiers.tsx +++ b/apps/webapp/app/components/billing/v2/PricingTiers.tsx @@ -2,17 +2,17 @@ import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid"; import { Form, useActionData, useNavigation } from "@remix-run/react"; -import { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/billing"; +import { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/platform/v2"; import { useState } from "react"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; import { cn } from "~/utils/cn"; import { formatNumberCompact } from "~/utils/numberFormatter"; -import { DefinitionTip } from "../DefinitionTooltip"; -import { Feedback } from "../Feedback"; -import { Button, LinkButton } from "../primitives/Buttons"; -import SegmentedControl from "../primitives/SegmentedControl"; +import { DefinitionTip } from "../../DefinitionTooltip"; +import { Feedback } from "../../Feedback"; +import { Button, LinkButton } from "../../primitives/Buttons"; +import SegmentedControl from "../../primitives/SegmentedControl"; import { RunsVolumeDiscountTable } from "./RunsVolumeDiscountTable"; -import { Spinner } from "../primitives/Spinner"; +import { Spinner } from "../../primitives/Spinner"; const pricingDefinitions = { concurrentRuns: { diff --git a/apps/webapp/app/components/billing/RunsVolumeDiscountTable.tsx b/apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx similarity index 90% rename from apps/webapp/app/components/billing/RunsVolumeDiscountTable.tsx rename to apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx index 2bd1b4455..d8215a409 100644 --- a/apps/webapp/app/components/billing/RunsVolumeDiscountTable.tsx +++ b/apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx @@ -1,6 +1,6 @@ -import { RunPriceBracket } from "@trigger.dev/billing"; -import { Header2 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; +import { RunPriceBracket } from "@trigger.dev/platform/v2"; +import { Header2 } from "../../primitives/Headers"; +import { Paragraph } from "../../primitives/Paragraph"; import { formatNumberCompact } from "~/utils/numberFormatter"; export function RunsVolumeDiscountTable({ diff --git a/apps/webapp/app/components/billing/UsageBar.tsx b/apps/webapp/app/components/billing/v2/UsageBar.tsx similarity index 98% rename from apps/webapp/app/components/billing/UsageBar.tsx rename to apps/webapp/app/components/billing/v2/UsageBar.tsx index 8cd580bc0..1a86f7021 100644 --- a/apps/webapp/app/components/billing/UsageBar.tsx +++ b/apps/webapp/app/components/billing/v2/UsageBar.tsx @@ -1,7 +1,7 @@ import { cn } from "~/utils/cn"; import { formatNumberCompact } from "~/utils/numberFormatter"; -import { Paragraph } from "../primitives/Paragraph"; -import { SimpleTooltip } from "../primitives/Tooltip"; +import { Paragraph } from "../../primitives/Paragraph"; +import { SimpleTooltip } from "../../primitives/Tooltip"; import { motion } from "framer-motion"; type UsageBarProps = { diff --git a/apps/webapp/app/components/billing/v3/UpgradePrompt.tsx b/apps/webapp/app/components/billing/v3/UpgradePrompt.tsx new file mode 100644 index 000000000..b4584216d --- /dev/null +++ b/apps/webapp/app/components/billing/v3/UpgradePrompt.tsx @@ -0,0 +1,53 @@ +import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import tileBgPath from "~/assets/images/error-banner-tile@2x.png"; +import { MatchedOrganization, useOrganization } from "~/hooks/useOrganizations"; +import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; +import { v3BillingPath } from "~/utils/pathBuilder"; +import { LinkButton } from "../../primitives/Buttons"; +import { Icon } from "../../primitives/Icon"; +import { Paragraph } from "../../primitives/Paragraph"; +import { DateTime } from "~/components/primitives/DateTime"; + +export function UpgradePrompt() { + const organization = useOrganization(); + const plan = useCurrentPlan(); + + if (!plan || !plan.v3Usage.hasExceededFreeTier) { + return null; + } + + const nextMonth = new Date(); + nextMonth.setMonth(nextMonth.getMonth() + 1); + nextMonth.setDate(1); + nextMonth.setHours(0, 0, 0, 0); + + return ( +
+
+ + + You have exceeded the monthly $ + {(plan.v3Subscription?.plan?.limits.includedUsage ?? 500) / 100} free credits. No runs + will execute in Prod until , or you + upgrade. + +
+ + Upgrade + +
+ ); +} + +export function useShowUpgradePrompt(organization?: MatchedOrganization) { + const currentPlan = useCurrentPlan(); + const shouldShow = currentPlan?.v3Usage.hasExceededFreeTier === true; + return { shouldShow }; +} diff --git a/apps/webapp/app/components/billing/v3/UsageBar.tsx b/apps/webapp/app/components/billing/v3/UsageBar.tsx new file mode 100644 index 000000000..c95f4d61f --- /dev/null +++ b/apps/webapp/app/components/billing/v3/UsageBar.tsx @@ -0,0 +1,170 @@ +import { cn } from "~/utils/cn"; +import { formatCurrency } from "~/utils/numberFormatter"; +import { Paragraph } from "../../primitives/Paragraph"; +import { SimpleTooltip } from "../../primitives/Tooltip"; +import { motion } from "framer-motion"; + +type UsageBarProps = { + current: number; + billingLimit?: number; + tierLimit?: number; + projectedUsage?: number; + isPaying: boolean; +}; + +const startFactor = 4; + +export function UsageBar({ + current, + billingLimit, + tierLimit, + projectedUsage, + isPaying, +}: UsageBarProps) { + const getLargestNumber = Math.max( + current, + tierLimit ?? -Infinity, + projectedUsage ?? -Infinity, + billingLimit ?? -Infinity, + 5 + ); + //creates a maximum range for the progress bar, add 10% to the largest number so the bar doesn't reach the end + const maxRange = Math.round(getLargestNumber * 1.1); + const tierRunLimitPercentage = tierLimit ? Math.round((tierLimit / maxRange) * 100) : 0; + const projectedRunsPercentage = projectedUsage + ? Math.round((projectedUsage / maxRange) * 100) + : 0; + const billingLimitPercentage = + billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0; + const usagePercentage = Math.round((current / maxRange) * 100); + + //cap the usagePercentage to the freeRunLimitPercentage + const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage); + + return ( +
+
+ {billingLimit && ( + + + + )} + {tierLimit && ( + + + + )} + {projectedUsage && projectedUsage !== 0 && ( + + + + )} + tierLimit ? "bg-rose-600" : "bg-green-600" + )} + > + + + +
+
+ ); +} + +const positions = { + topRow1: "bottom-0 h-9", + topRow2: "bottom-0 h-14", + bottomRow1: "top-0 h-9 items-end", + bottomRow2: "top-0 h-14 items-end", +}; + +type LegendProps = { + text: string; + value: number | string; + percentage: number; + position: keyof typeof positions; + tooltipContent: string; +}; + +function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) { + const flipLegendPositionValue = 80; + const flipLegendPosition = percentage > flipLegendPositionValue ? true : false; + return ( +
+ + {text} + {value} + + } + side="top" + content={tooltipContent} + className="z-50 h-fit" + /> +
+ ); +} diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index 2035e722d..1c2cf2036 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -59,7 +59,7 @@ const extraLinesWhenClipping = 0.35; const defaultTheme: PrismTheme = { plain: { color: "#9C9AF2", - backgroundColor: "#121317", + backgroundColor: "rgba(0, 0, 0, 0)", }, styles: [ { diff --git a/apps/webapp/app/components/layout/AppLayout.tsx b/apps/webapp/app/components/layout/AppLayout.tsx index 1748337eb..e6345204a 100644 --- a/apps/webapp/app/components/layout/AppLayout.tsx +++ b/apps/webapp/app/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ import { useOptionalOrganization } from "~/hooks/useOrganizations"; import { cn } from "~/utils/cn"; -import { useShowUpgradePrompt } from "../billing/UpgradePrompt"; +import { useShowUpgradePrompt } from "../billing/v3/UpgradePrompt"; /** This container is used to surround the entire app, it correctly places the nav bar */ export function AppContainer({ children }: { children: React.ReactNode }) { @@ -60,7 +60,9 @@ export function MainCenteredContainer({ }) { return (
-
{children}
+
+ {children} +
); } diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 3159b848a..46d9ae7d5 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -1,16 +1,17 @@ import { AcademicCapIcon, - ArrowRightIcon, ArrowRightOnRectangleIcon, BeakerIcon, BellAlertIcon, ChartBarIcon, ClockIcon, + CreditCardIcon, CursorArrowRaysIcon, IdentificationIcon, KeyIcon, ServerStackIcon, ShieldCheckIcon, + SignalIcon, } from "@heroicons/react/20/solid"; import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; import { useNavigation } from "@remix-run/react"; @@ -44,6 +45,7 @@ import { projectSetupPath, projectTriggersPath, v3ApiKeysPath, + v3BillingPath, v3DeploymentsPath, v3EnvironmentVariablesPath, v3ProjectAlertsPath, @@ -52,13 +54,14 @@ import { v3RunsPath, v3SchedulesPath, v3TestPath, + v3UsagePath, } from "~/utils/pathBuilder"; import { Feedback } from "../Feedback"; import { ImpersonationBanner } from "../ImpersonationBanner"; import { LogoIcon } from "../LogoIcon"; import { StepContentContainer } from "../StepContentContainer"; import { UserProfilePhoto } from "../UserProfilePhoto"; -import { FreePlanUsage } from "../billing/FreePlanUsage"; +import { FreePlanUsage } from "../billing/v2/FreePlanUsage"; import { Badge } from "../primitives/Badge"; import { Button } from "../primitives/Buttons"; import { Callout } from "../primitives/Callout"; @@ -75,6 +78,7 @@ import { PopoverSectionHeader, } from "../primitives/Popover"; import { StepNumber } from "../primitives/StepNumber"; +import { TextLink } from "../primitives/TextLink"; import { SideMenuHeader } from "./SideMenuHeader"; import { MenuCount, SideMenuItem } from "./SideMenuItem"; @@ -100,8 +104,8 @@ type SideMenuProps = { export function SideMenu({ user, project, organization, organizations }: SideMenuProps) { const borderRef = useRef(null); const [showHeaderDivider, setShowHeaderDivider] = useState(false); - const { isManagedCloud } = useFeatures(); const currentPlan = useCurrentPlan(); + const { isManagedCloud } = useFeatures(); useEffect(() => { const handleScroll = () => { @@ -173,16 +177,36 @@ export function SideMenu({ user, project, organization, organizations }: SideMen name="Team" icon={UserGroupIcon} to={organizationTeamPath(organization)} - iconColor="text-sky-500" + iconColor="text-amber-500" data-action="team" /> - + {organization.projects.some((proj) => proj.version === "V3") && isManagedCloud && ( + <> + + + + )} + {organization.projects.some((proj) => proj.version === "V2") && ( + + )}
- {project.version === "V2" ? ( - This is a v2 project - ) : ( - This is a v3 project in Developer Preview + {project.version === "V2" && ( + + + This is a v2 project.{" "} + + Upgrade to v3 + + + )}
- {project.version === "V2" && ( - - )} - {currentPlan?.subscription?.isPaying === true && ( + {currentPlan?.v3Subscription?.plan?.limits.support === "slack" && ( - } - /> - ) : ( - - Give feedback on v3 - - } - /> - )} - {currentPlan && !currentPlan.subscription?.isPaying && currentPlan.usage.runCountCap && ( + + + Help & Feedback + + } + /> + {currentPlan?.v3Subscription?.isPaying === false && ( )}
@@ -474,7 +475,7 @@ function V2ProjectSideMenu({ }) { return ( <> - + - + ); } - -function V3Icon() { - return ( - - - - - ); -} diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index e29299194..6d8c9d553 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -21,7 +21,7 @@ const sizes = { shortcut: "-ml-0.5 -mr-1.5 rounded justify-self-center", }, large: { - button: "h-10 px-2 text-sm font-medium", + button: "h-10 px-2 text-base font-medium", icon: "h-5", iconSpacing: "gap-x-0.5", shortcutVariant: "medium" as const, diff --git a/apps/webapp/app/components/primitives/Chart.tsx b/apps/webapp/app/components/primitives/Chart.tsx new file mode 100644 index 000000000..53d53c0ea --- /dev/null +++ b/apps/webapp/app/components/primitives/Chart.tsx @@ -0,0 +1,324 @@ +"use client"; + +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; +import { cn } from "~/utils/cn"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + {children} +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color); + + if (!colorConfig.length) { + return null; + } + + return ( +