diff --git a/apps/webapp/app/assets/icons/PadlockRoundedIcon.tsx b/apps/webapp/app/assets/icons/PadlockRoundedIcon.tsx new file mode 100644 index 00000000000..56f2543fc3f --- /dev/null +++ b/apps/webapp/app/assets/icons/PadlockRoundedIcon.tsx @@ -0,0 +1,34 @@ +export function PadlockRoundedIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 5d166158e0b..5fde85cc050 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -701,14 +701,12 @@ function DeploymentOnboardingSteps() { Deploy automatically with every push. Read the{" "} full guide. -
- -
+ diff --git a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx index 1abd54b0121..fc45d2f6019 100644 --- a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx +++ b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx @@ -1,5 +1,12 @@ import { Switch } from "~/components/primitives/Switch"; +import { LinkButton } from "~/components/primitives/Buttons"; import { Label } from "~/components/primitives/Label"; +import { + SettingsRow, + SettingsRowDescription, + SettingsRowTitle, +} from "~/components/primitives/SettingsLayout"; +import { cn } from "~/utils/cn"; import { Hint } from "~/components/primitives/Hint"; import { TextLink } from "~/components/primitives/TextLink"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; @@ -31,6 +38,7 @@ type BuildSettingsFieldsProps = { currentTriggerVersionFetchFailed?: boolean; /** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */ hideSectionToggles?: boolean; + layout?: "settings" | "card"; }; export function BuildSettingsFields({ @@ -48,187 +56,326 @@ export function BuildSettingsFields({ currentTriggerVersion, currentTriggerVersionFetchFailed, hideSectionToggles, + layout = "card", }: BuildSettingsFieldsProps) { const isSlugDisabled = (slug: EnvSlug) => !!disabledEnvSlugs?.[slug]; const enabledSlugs = availableEnvSlugs.filter((s) => !isSlugDisabled(s)); + const envVarSections = + layout === "settings" ? ( + <> + + Configure env vars + + ) : undefined + } + /> + {availableEnvSlugs.map((slug) => ( + { + onPullEnvVarsChange( + checked + ? [...pullEnvVarsBeforeBuild, slug] + : pullEnvVarsBeforeBuild.filter((s) => s !== slug) + ); + }} + /> + ))} + + + {availableEnvSlugs.map((slug) => { + const pullOff = !pullEnvVarsBeforeBuild.includes(slug); + return ( + { + onDiscoverEnvVarsChange( + checked ? [...discoverEnvVars, slug] : discoverEnvVars.filter((s) => s !== slug) + ); + }} + /> + ); + })} + + ) : null; + + const atomicSections = + layout === "settings" ? ( + <> + { + onAtomicBuildsChange(checked ? ["prod"] : []); + }} + /> + } + > +
+ Atomic deployments + + Promotes your Vercel deployment and your tasks together in Production, so your app + never runs against a mismatched task version. Requires turning off "Auto-assign Custom + Production Domains" on your Vercel project, which Trigger.dev does for you.{" "} + + Learn more + + . + + {currentTriggerVersion && ( + + Currently pinned to{" "} + {currentTriggerVersion} in + Vercel production. + + )} + {!currentTriggerVersion && currentTriggerVersionFetchFailed && ( + + Couldn't read TRIGGER_VERSION{" "} + from Vercel. Check the Vercel dashboard to confirm the production pin. + + )} +
+
+ + {atomicBuilds.includes("prod") && onAutoPromoteChange !== undefined && ( + + } + /> + )} + + ) : null; + return ( <> + {envVarSections} {/* Pull env vars before build */} -
-
-
- - {!hideSectionToggles && availableEnvSlugs.length > 1 && ( - 0 && - enabledSlugs.every((s) => pullEnvVarsBeforeBuild.includes(s)) - } - onCheckedChange={(checked) => { - onPullEnvVarsChange(checked ? [...enabledSlugs] : []); - }} - /> - )} -
- - Select which environments should pull environment variables from Vercel before each - build.{" "} - {envVarsConfigLink && ( - <> - Configure which variables to pull. - - )} - -
-
- {availableEnvSlugs.map((slug) => { - const envType = envSlugToType(slug); - const disabled = isSlugDisabled(slug); - const disabledReason = disabledEnvSlugs?.[slug]; - const row = ( -
-
- - - {environmentFullTitle({ type: envType })} - -
+ {layout === "card" && ( +
+
+
+ + {!hideSectionToggles && availableEnvSlugs.length > 1 && ( 0 && + enabledSlugs.every((s) => pullEnvVarsBeforeBuild.includes(s)) + } onCheckedChange={(checked) => { - onPullEnvVarsChange( - checked - ? [...pullEnvVarsBeforeBuild, slug] - : pullEnvVarsBeforeBuild.filter((s) => s !== slug) - ); + onPullEnvVarsChange(checked ? [...enabledSlugs] : []); }} /> -
- ); - if (disabled && disabledReason) { - return ; - } - return row; - })} + )} +
+ + Select which environments should pull environment variables from Vercel before each + build.{" "} + {envVarsConfigLink && ( + <> + Configure which variables to pull. + + )} + +
+
+ {availableEnvSlugs.map((slug) => { + const envType = envSlugToType(slug); + const disabled = isSlugDisabled(slug); + const disabledReason = disabledEnvSlugs?.[slug]; + const row = ( +
+
+ + + {environmentFullTitle({ type: envType })} + +
+ { + onPullEnvVarsChange( + checked + ? [...pullEnvVarsBeforeBuild, slug] + : pullEnvVarsBeforeBuild.filter((s) => s !== slug) + ); + }} + /> +
+ ); + if (disabled && disabledReason) { + return ( + + ); + } + return row; + })} +
-
+ )} {/* Discover new env vars */} -
-
-
- - {!hideSectionToggles && availableEnvSlugs.length > 1 && ( - 0 && - enabledSlugs.every( - (s) => discoverEnvVars.includes(s) || !pullEnvVarsBeforeBuild.includes(s) - ) && - enabledSlugs.some((s) => discoverEnvVars.includes(s)) - } - disabled={!enabledSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))} - onCheckedChange={(checked) => { - onDiscoverEnvVarsChange( - checked ? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s)) : [] - ); - }} - /> - )} -
- - Select which environments should automatically discover and create new environment - variables from Vercel during builds. - -
-
- {availableEnvSlugs.map((slug) => { - const envType = envSlugToType(slug); - const disabled = isSlugDisabled(slug); - const disabledReason = disabledEnvSlugs?.[slug]; - const isPullDisabled = !pullEnvVarsBeforeBuild.includes(slug); - const row = ( -
-
- - - {environmentFullTitle({ type: envType })} - -
+ {layout === "card" && ( +
+
+
+ + {!hideSectionToggles && availableEnvSlugs.length > 1 && ( 0 && + enabledSlugs.every( + (s) => discoverEnvVars.includes(s) || !pullEnvVarsBeforeBuild.includes(s) + ) && + enabledSlugs.some((s) => discoverEnvVars.includes(s)) + } + disabled={!enabledSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))} onCheckedChange={(checked) => { onDiscoverEnvVarsChange( - checked - ? [...discoverEnvVars, slug] - : discoverEnvVars.filter((s) => s !== slug) + checked ? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s)) : [] ); }} /> -
- ); - if (disabled && disabledReason) { - return ; - } - return row; - })} + )} +
+ + Select which environments should automatically discover and create new environment + variables from Vercel during builds. + +
+
+ {availableEnvSlugs.map((slug) => { + const envType = envSlugToType(slug); + const disabled = isSlugDisabled(slug); + const disabledReason = disabledEnvSlugs?.[slug]; + const isPullDisabled = !pullEnvVarsBeforeBuild.includes(slug); + const row = ( +
+
+ + + {environmentFullTitle({ type: envType })} + +
+ { + onDiscoverEnvVarsChange( + checked + ? [...discoverEnvVars, slug] + : discoverEnvVars.filter((s) => s !== slug) + ); + }} + /> +
+ ); + if (disabled && disabledReason) { + return ( + + ); + } + return row; + })} +
-
+ )} + + {atomicSections} {/* Atomic deployments */} -
-
- - { - onAtomicBuildsChange(checked ? ["prod"] : []); - }} - /> -
- - When enabled, production deployments wait for Vercel deployment to complete before - promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom Production - Domains" option in your Vercel project settings to perform staged deployments.{" "} - - Learn more - - . - - {currentTriggerVersion && ( + {layout === "card" && ( +
+
+ + { + onAtomicBuildsChange(checked ? ["prod"] : []); + }} + /> +
- Currently pinned to{" "} - {currentTriggerVersion} in Vercel - production. + When enabled, production deployments wait for Vercel deployment to complete before + promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom + Production Domains" option in your Vercel project settings to perform staged + deployments.{" "} + + Learn more + + . - )} - {!currentTriggerVersion && currentTriggerVersionFetchFailed && ( - - Couldn't read TRIGGER_VERSION from - Vercel — check the Vercel dashboard to confirm the production pin. - - )} -
+ {currentTriggerVersion && ( + + Currently pinned to{" "} + {currentTriggerVersion} in Vercel + production. + + )} + {!currentTriggerVersion && currentTriggerVersionFetchFailed && ( + + Couldn't read TRIGGER_VERSION from + Vercel — check the Vercel dashboard to confirm the production pin. + + )} +
+ )} {/* Auto promotion — only visible when atomic deployments are on */} - {atomicBuilds.includes("prod") && onAutoPromoteChange !== undefined && ( + {layout === "card" && atomicBuilds.includes("prod") && onAutoPromoteChange !== undefined && (
@@ -248,3 +395,59 @@ export function BuildSettingsFields({ ); } + +function EnvToggleRow({ + slug, + checked, + disabled, + disabledReason, + unlockHint, + unlockTarget, + onCheckedChange, +}: { + slug: EnvSlug; + checked: boolean; + disabled: boolean; + disabledReason?: string; + unlockHint?: string; + unlockTarget?: string; + onCheckedChange: (checked: boolean) => void; +}) { + const envType = envSlugToType(slug); + + return ( + + + + } + > +
+
+ + + {environmentFullTitle({ type: envType })} + +
+ {disabled && disabledReason ? ( + {disabledReason} + ) : null} +
+
+ ); +} diff --git a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx index 85c7816b73e..cd5f9eee505 100644 --- a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx +++ b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx @@ -13,6 +13,7 @@ import { FormButtons } from "~/components/primitives/FormButtons"; import { FormError } from "~/components/primitives/FormError"; import { Header3 } from "~/components/primitives/Headers"; import { Hint } from "~/components/primitives/Hint"; +import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Select, SelectItem } from "~/components/primitives/Select"; @@ -797,10 +798,8 @@ export function VercelOnboardingModal({
{showProjectSelection && (
- Select Vercel Project - - Choose which Vercel project to connect with this Trigger.dev project. Your API keys - will be automatically synced to Vercel. + + Choose the Vercel project to pair with this Trigger.dev project. {availableProjects.length === 0 ? ( @@ -808,41 +807,42 @@ export function VercelOnboardingModal({ No Vercel projects found. Please create a project in Vercel first. ) : ( - + + + + Your{" "} + + TRIGGER_SECRET_KEY + {" "} + is synced to Vercel for each environment once connected. + + )} {projectSelectionError && {projectSelectionError}} - - Once connected, your{" "} - - TRIGGER_SECRET_KEY - {" "} - will be automatically synced to Vercel for each environment. - - @@ -866,7 +866,7 @@ export function VercelOnboardingModal({
} cancelButton={ - } diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 323f3743826..6075d52f1bf 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -89,6 +89,13 @@ const theme = { shortcut: "border-text-bright text-text-bright group-hover/button:border-text-bright/60", icon: "text-text-bright", }, + warning: { + textColor: "text-warning transition group-disabled/button:text-warning/60", + button: + "bg-warning/10 border border-warning/20 group-hover/button:bg-warning/20 group-hover/button:border-warning/40 group-disabled/button:opacity-60 group-disabled/button:pointer-events-none", + shortcut: "border-warning/40 text-warning group-hover/button:border-warning/60", + icon: "text-warning", + }, docs: { textColor: "text-blue-200/70 transition group-disabled/button:text-text-dimmed/80", button: @@ -133,6 +140,10 @@ const variant = { "danger/medium": createVariant("medium", "danger"), "danger/large": createVariant("large", "danger"), "danger/extra-large": createVariant("extra-large", "danger"), + "warning/small": createVariant("small", "warning"), + "warning/medium": createVariant("medium", "warning"), + "warning/large": createVariant("large", "warning"), + "warning/extra-large": createVariant("extra-large", "warning"), "docs/small": createVariant("small", "docs"), "docs/medium": createVariant("medium", "docs"), "docs/large": createVariant("large", "docs"), diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index 66b291b7524..3c4c8faad0c 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -38,6 +38,9 @@ const variants = { "secondary/small": { button: cn(sizes.small.button, style.secondary.button), }, + "secondary/medium": { + button: cn(sizes.medium.button, style.secondary.button), + }, "tertiary/small": { button: cn(sizes.small.button, style.tertiary.button), }, @@ -692,11 +695,11 @@ export function ComboBox({ ...props }: ComboBoxProps) { return ( -
+
} - className="flex-1 bg-transparent text-xs text-text-dimmed outline-hidden" + className="flex-1 border-0 bg-transparent text-xs text-text-dimmed outline-hidden focus:border-0 focus:ring-0" {...props} /> {shortcut && ( diff --git a/apps/webapp/app/components/primitives/SettingsLayout.tsx b/apps/webapp/app/components/primitives/SettingsLayout.tsx index 7d6484fb467..dae340e9835 100644 --- a/apps/webapp/app/components/primitives/SettingsLayout.tsx +++ b/apps/webapp/app/components/primitives/SettingsLayout.tsx @@ -1,3 +1,4 @@ +import { ExclamationCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { type ReactNode } from "react"; import { MainHorizontallyCenteredContainer } from "~/components/layout/AppLayout"; import { cn } from "~/utils/cn"; @@ -100,7 +101,10 @@ export function SettingsRowTitle({ htmlFor?: string; className?: string; }) { - const classes = cn("font-sans text-sm font-semibold leading-tight text-text-bright", className); + const classes = cn( + "block font-sans text-sm font-semibold leading-tight text-text-bright", + className + ); return htmlFor ? (
@@ -335,6 +341,12 @@ export default function Story() { /> This is a delete button +
diff --git a/apps/webapp/app/routes/storybook.icons/route.tsx b/apps/webapp/app/routes/storybook.icons/route.tsx index a465e05fb5a..36acd29b234 100644 --- a/apps/webapp/app/routes/storybook.icons/route.tsx +++ b/apps/webapp/app/routes/storybook.icons/route.tsx @@ -93,6 +93,7 @@ import { MoveUpIcon } from "~/assets/icons/MoveUpIcon"; import { NodejsLogoIcon } from "~/assets/icons/NodejsLogoIcon"; import { OneTreeIcon } from "~/assets/icons/OneTreeIcon"; import { PadlockIcon } from "~/assets/icons/PadlockIcon"; +import { PadlockRoundedIcon } from "~/assets/icons/PadlockRoundedIcon"; import { PauseIcon } from "~/assets/icons/PauseIcon"; import { PlaygroundIcon } from "~/assets/icons/PlaygroundIcon"; import { PlusIcon } from "~/assets/icons/PlusIcon"; @@ -233,6 +234,7 @@ const icons: IconEntry[] = [ { name: "OneTreeIcon", render: simple(OneTreeIcon) }, { name: "OpenAIIcon", render: simple(OpenAIIcon) }, { name: "PadlockIcon", render: simple(PadlockIcon) }, + { name: "PadlockRoundedIcon", render: simple(PadlockRoundedIcon) }, { name: "PauseIcon", render: simple(PauseIcon) }, { name: "PerplexityIcon", render: simple(PerplexityIcon) }, { name: "PlaygroundIcon", render: simple(PlaygroundIcon) }, diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 2bc91d77323..ab845b63353 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -639,3 +639,13 @@ @apply leading-relaxed; } } + +form:has(.unlock-hint-staging-env:hover) [data-unlock-target="staging-env"], +form:has(.unlock-hint-pull-prod:hover) [data-unlock-target="pull-prod"], +form:has(.unlock-hint-pull-stg:hover) [data-unlock-target="pull-stg"], +form:has(.unlock-hint-pull-preview:hover) [data-unlock-target="pull-preview"], +form:has(.unlock-hint-pull-dev:hover) [data-unlock-target="pull-dev"] { + border-radius: 0.25rem; + outline: 2px dashed var(--color-warning); + outline-offset: 2px; +}