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 ( +