V3 style improvements (#926)

* Added more clipboard variants to storybook and improved styling

* Improvements to the clipboard components

* Code blocks match the design

* Improved the code block styles

* Dialog modal improvements

* Improved the free plan usage bar

* Better alignment of the shortcut icon in the button

* Side menu displays ‘v3’ next to the project header

* Page header improvements

* Project dropdown displays a badge if it’s a v3 project

* billing page now shows message

* Added the v3 badge to the project list page

* version 3 projects skip the select plan onboarding

* Added a new blank state to the runs page

* Conditionally set the blank state on the runs page

* Button padding tweak

* Updated the task and attempt icons

* Added a new blank state to the runs page

* Added more buttons variants to the button storybook

* Added new blank onboarding state for the tasks page

* Added a blank state to the test page

* Fix for bad svg attribute in TaskIcon

* Onboarding tweaks

* Add the projectRef to the init command

* Task icon is blue

* Added a v3 Project settings page

* Fix for typecheck error

* Added the new CodeBlocks with rowTitles to the span panel

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
James Ritchie
2024-03-06 18:06:20 +00:00
committed by GitHub
parent 9b6f8f9238
commit 161b2b3323
30 changed files with 935 additions and 261 deletions
+24 -7
View File
@@ -1,12 +1,29 @@
export function AttemptIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M9.0711 0.585818C8.29001 -0.195272 7.02361 -0.195273 6.24252 0.585818L0.585818 6.24252C-0.195272 7.02361 -0.195273 8.29001 0.585818 9.0711L6.24252 14.7278C7.02361 15.5089 8.29001 15.5089 9.0711 14.7278L14.7278 9.0711C15.5089 8.29001 15.5089 7.02361 14.7278 6.24252L9.0711 0.585818ZM6.85833 4.07179L4.29833 11.1718H5.65833L6.24833 9.49179H9.05833L9.64833 11.1718H11.0083L8.44833 4.07179H6.85833ZM8.67833 8.38179H6.62833L7.65833 5.38179L8.67833 8.38179Z"
/>
<svg
className={className}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_9964_113464)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 0H0V16H16V0ZM7.09906 4.4L4.53906 11.5H6.11906L6.63906 10H9.35906L9.87906 11.5H11.4591L8.89906 4.4H7.09906ZM7.99906 6L8.92906 8.73H7.06906L7.99906 6Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_9964_113464">
<path
d="M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2Z"
fill="white"
/>
</clipPath>
</defs>
</svg>
);
}
+12 -11
View File
@@ -1,22 +1,23 @@
export function TaskIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.0711 0.585818C8.29001 -0.195272 7.02361 -0.195273 6.24252 0.585818L0.585818 6.24252C-0.195272 7.02361 -0.195273 8.29001 0.585818 9.0711L6.24252 14.7278C7.02361 15.5089 8.29001 15.5089 9.0711 14.7278L14.7278 9.0711C15.5089 8.29001 15.5089 7.02361 14.7278 6.24252L9.0711 0.585818ZM6.85833 4.07179L4.29833 11.1718H5.65833L6.24833 9.49179H9.05833L9.64833 11.1718H11.0083L8.44833 4.07179H6.85833ZM8.67833 8.38179H6.62833L7.65833 5.38179L8.67833 8.38179Z"
/>
<g clipPath="url(#clip0_8651_301570)">
<svg
className={className}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_9221_99732)">
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M16 0H0V16H16V0ZM10.8099 6.05039V4.90039H5.18994V6.05039H7.33994V12.0004H8.65994V6.05039H10.8099Z"
d="M16 0H0V16H16V0ZM10.8901 5.73995V4.44995H5.11011V5.73995H7.23011V11.55H8.77011V5.73995H10.8901Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_8651_301570">
<clipPath id="clip0_9221_99732">
<path
d="M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2Z"
fill="white"
@@ -0,0 +1,23 @@
import { cn } from "~/utils/cn";
import { Header2 } from "./primitives/Headers";
export function BlankstateInstructions({
children,
className,
title,
}: {
children: React.ReactNode;
className?: string;
title?: string;
}) {
return (
<div className={cn("flex flex-col gap-2", className)}>
{title && (
<div className="flex h-10 items-center border-b border-grid-bright">
<Header2>{title}</Header2>
</div>
)}
{children}
</div>
);
}
@@ -1,3 +1,4 @@
import { useProject } from "~/hooks/useProject";
import { InlineCode } from "./code/InlineCode";
import {
ClientTabs,
@@ -128,3 +129,79 @@ export function TriggerDevStep({ extra }: { extra?: string }) {
</>
);
}
// Trigger.dev version 3 setup commands
export function InitCommandV3() {
const project = useProject();
const projectRef = `proj_${project.ref}`;
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`npx trigger.dev@latest init -p ${projectRef}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`pnpm dlx trigger.dev@latest init -p ${projectRef}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`yarn dlx trigger.dev@latest init -p ${projectRef}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
export function TriggerDevStepV3() {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`npx trigger.dev@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`pnpm dlx trigger.dev@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`yarn dlx trigger.dev@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
@@ -19,7 +19,7 @@ export function FreePlanUsage({ to, percentage }: { to: string; percentage: numb
<div
className={cn(
"rounded border border-charcoal-700 bg-charcoal-750 p-2.5",
hasHitLimit && "border-rose-800/60"
hasHitLimit && "border-error/40"
)}
>
<div className="flex items-center justify-between gap-2">
@@ -27,11 +27,11 @@ export function FreePlanUsage({ to, percentage }: { to: string; percentage: numb
<ArrowUpCircleIcon className="h-5 w-5 text-text-dimmed" />
<Paragraph className="text-2sm text-text-bright">Free Plan</Paragraph>
</div>
<Link to={to} className="text-2sm text-indigo-500">
<Link to={to} className="text-2sm text-text-link">
Learn more
</Link>
</div>
<div className="relative mt-3 h-1 rounded-full bg-[#0B1018]">
<div className="relative mt-3 h-1 rounded-full bg-background-dimmed">
<motion.div
initial={{ width: 0 }}
animate={{ width: cappedPercentage * 100 + "%" }}
+19 -10
View File
@@ -4,6 +4,7 @@ import Highlight, { defaultProps } from "prism-react-renderer";
import { forwardRef, useCallback, useState } from "react";
import { cn } from "~/utils/cn";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { Paragraph } from "../primitives/Paragraph";
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
@@ -38,6 +39,9 @@ type CodeBlockProps = {
/** filename */
fileName?: string;
/** title text for the Title row */
rowTitle?: string;
};
const dimAmount = 0.5;
@@ -46,7 +50,7 @@ const extraLinesWhenClipping = 0.35;
const defaultTheme: PrismTheme = {
plain: {
color: "#9CDCFE",
backgroundColor: "#1A1B1F",
backgroundColor: "rgba(0, 0, 0, 0)",
},
styles: [
{
@@ -160,6 +164,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
maxLines,
showChrome = false,
fileName,
rowTitle,
...props
}: CodeBlockProps,
ref
@@ -196,7 +201,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
return (
<div
className={cn("relative overflow-hidden rounded-md border border-charcoal-800", className)}
className={cn("relative overflow-hidden rounded-md border border-grid-bright", className)}
style={{
backgroundColor: theme.plain.backgroundColor,
}}
@@ -205,6 +210,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
translate="no"
>
{showChrome && <Chrome title={fileName} />}
{rowTitle && <TitleRow title={rowTitle} />}
{showCopyButton && (
<TooltipProvider>
<Tooltip open={copied || mouseOver}>
@@ -354,16 +360,19 @@ function Chrome({ title }: { title?: string }) {
<div className="h-3 w-3 rounded-full bg-charcoal-700" />
</div>
<div className="flex items-center justify-center">
<div
className={cn(
"rounded-sm px-3 py-0.5 text-xs text-charcoal-500",
title && "bg-background-dimmed"
)}
>
{title}
</div>
<div className={cn("rounded-sm px-3 py-0.5 text-xs text-charcoal-500")}>{title}</div>
</div>
<div></div>
</div>
);
}
function TitleRow({ title }: { title: string }) {
return (
<div className="flex items-center justify-between px-4">
<Paragraph variant="base/bright" className="w-full border-b border-grid-dimmed py-2.5">
{title}
</Paragraph>
</div>
);
}
@@ -42,6 +42,7 @@ import {
v3ApiKeysPath,
v3EnvironmentVariablesPath,
v3ProjectPath,
v3ProjectSettingsPath,
v3RunsPath,
v3TestPath,
} from "~/utils/pathBuilder";
@@ -67,6 +68,8 @@ import {
import { StepNumber } from "../primitives/StepNumber";
import { SideMenuHeader } from "./SideMenuHeader";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
import { Badge } from "../primitives/Badge";
import { TaskIcon } from "~/assets/icons/TaskIcon";
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
type SideMenuProject = Pick<
@@ -247,13 +250,24 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
data-action="join our discord"
target="_blank"
/>
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
{project.version === "V2" ? (
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
) : (
<SideMenuItem
name="Documentation (v3)"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
)}
<SideMenuItem
name="Changelog"
icon="star"
@@ -330,7 +344,11 @@ function ProjectSelector({
title={
<div className="flex w-full items-center justify-between text-text-bright">
<span className="grow truncate text-left">{p.name}</span>
<MenuCount count={p.jobCount} />
{p.version === "V2" ? (
<MenuCount count={p.jobCount} />
) : (
<Badge variant="v3">v3</Badge>
)}
</div>
}
isSelected={isSelected}
@@ -497,11 +515,11 @@ function V3ProjectSideMenu({
}) {
return (
<>
<SideMenuHeader title={"Project"} />
<SideMenuHeader title={"Project (v3)"} />
<SideMenuItem
name="Tasks"
icon="job"
iconColor="text-indigo-500"
icon={TaskIcon}
iconColor="text-blue-500"
count={project.jobCount}
to={v3ProjectPath(organization, project)}
data-action="tasks"
@@ -533,14 +551,13 @@ function V3ProjectSideMenu({
to={v3EnvironmentVariablesPath(organization, project)}
data-action="environment variables"
/>
{/*
<SideMenuItem
name="Project settings"
icon="settings"
iconColor="text-teal-500"
to={projectSettingsPath(organization, project)}
to={v3ProjectSettingsPath(organization, project)}
data-action="project-settings"
/> */}
/>
</>
);
}
@@ -11,14 +11,14 @@ const sizes = {
icon: "h-3.5 -mx-1",
iconSpacing: "gap-x-2.5",
shortcutVariant: "small" as const,
shortcut: "ml-2 -mr-1.5 justify-self-center",
shortcut: "-ml-0.5 -mr-1.5 justify-self-center",
},
medium: {
button: "h-8 px-2.5 text-sm",
button: "h-8 px-3 text-sm",
icon: "h-4 -mx-1",
iconSpacing: "gap-x-2.5",
shortcutVariant: "medium" as const,
shortcut: "ml-2 -mr-1.5 rounded justify-self-center",
shortcut: "-ml-0.5 -mr-1.5 rounded justify-self-center",
},
large: {
button: "h-10 px-2 text-sm font-medium",
@@ -3,66 +3,66 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "./Buttons";
import { IconNames, NamedIcon } from "./NamedIcon";
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
const variants = {
"primary/small": {
container:
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-850 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-charcoal-750 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "primary/small" as const,
button: "rounded-l-none min-w-[3.1rem]",
button: "rounded-l-none",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"secondary/small": {
container:
"flex items-center text-text-dimmed font-mono rounded border bg-transparent text-xs transition hover:bg-charcoal-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-xs transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-charcoal-750 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/small" as const,
button: "rounded-l-none border-l border-charcoal-750 min-w-[3.1rem]",
button: "rounded-l-none border-l border-charcoal-750",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"tertiary/small": {
container:
"group flex items-center text-text-dimmed font-mono rounded bg-transparent text-xs transition hover:border-charcoal-700 hover:bg-tertiary focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
"group flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-xs transition duration-150 hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "minimal/small" as const,
button:
"rounded-l-none border-l border-transparent group-hover:border-charcoal-700 min-w-[3.1rem]",
button: "rounded-l-none border-l border-transparent transition group-hover:border-charcoal-700",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"primary/medium": {
container:
"flex items-center text-text-dimmed font-mono rounded border border-charcoal-800 bg-charcoal-850 text-sm transition hover:border-charcoal-750 hover:bg-charcoal-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
"flex items-center text-text-dimmed font-mono rounded border bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-charcoal-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "primary/medium" as const,
button: "rounded-l-none min-w-[4rem]",
button: "rounded-l-none",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"secondary/medium": {
container:
"flex items-center text-text-dimmed font-mono rounded border border-charcoal-750 bg-charcoal-850 text-sm transition hover:border-charcoal-750 hover:bg-charcoal-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
"flex items-center text-text-dimmed font-mono rounded bg-charcoal-750 text-sm transition hover:bg-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-charcoal-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "secondary/medium" as const,
button: "rounded-l-none border-l border-charcoal-750 min-w-[4rem]",
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/medium" as const,
button: "rounded-l-none border-l border-charcoal-750",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"tertiary/medium": {
container:
"flex items-center text-text-dimmed font-mono rounded border border-charcoal-850 bg-transparent text-sm transition hover:border-charcoal-800 hover:bg-charcoal-950 focus:border-4 focus:border-solid focus:border-l-transparent",
"group flex items-center text-text-dimmed font-mono rounded bg-transparent border border-transparent text-sm transition hover:border-charcoal-700 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/medium" as const,
button: "rounded-l-none border-l border-charcoal-850 min-w-[4rem]",
buttonVariant: "minimal/medium" as const,
button: "rounded-l-none border-l border-transparent transition group-hover:border-charcoal-700",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
@@ -74,6 +74,7 @@ type ClipboardFieldProps = {
variant: keyof typeof variants;
className?: string;
icon?: IconNames | React.ReactNode;
iconButton?: boolean;
fullWidth?: boolean;
};
@@ -83,6 +84,7 @@ export function ClipboardField({
variant,
className,
icon,
iconButton = false,
fullWidth = true,
}: ClipboardFieldProps) {
const [isSecure, setIsSecure] = useState(secure !== undefined && secure);
@@ -139,9 +141,37 @@ export function ClipboardField({
}
}}
/>
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
</Button>
{iconButton ? (
<Button
variant={buttonVariant}
onClick={copy}
className={cn("shrink grow-0 px-1.5", button)}
>
{copied ? (
<ClipboardCheckIcon
className={cn(
"h-4 w-4",
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
? "text-background-dimmed"
: "text-green-500"
)}
/>
) : (
<ClipboardIcon
className={cn(
"h-4 w-4",
buttonVariant === "primary/small" || buttonVariant === "primary/medium"
? "text-background-dimmed"
: "text-text-dimmed"
)}
/>
)}
</Button>
) : (
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
{copied ? <CheckIcon className="mx-[0.4rem] h-4 w-4 text-green-500" /> : "Copy"}
</Button>
)}
</span>
);
}
@@ -43,12 +43,12 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background-dimmed px-4 pb-6 pt-3 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background-dimmed px-4 pb-6 pt-3.5 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className
)}
{...props}
>
<hr className="absolute left-0 top-12 w-full" />
<hr className="absolute left-0 top-11 w-full" />
{children}
<DialogPrimitive.Close className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus-visible:ring-ring absolute right-3 top-3 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none">
<div className="flex gap-x-2">
@@ -94,7 +94,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
className={cn("text-base font-normal leading-none tracking-tight text-text-bright", className)}
{...props}
/>
));
@@ -106,7 +106,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-muted-foreground pt-4 text-sm", className)}
{...props}
/>
));
@@ -1,5 +1,4 @@
import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
import { Link, useNavigation } from "@remix-run/react";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
@@ -7,11 +6,11 @@ import { plansPath } from "~/utils/pathBuilder";
import { UpgradePrompt, useShowUpgradePrompt } from "../billing/UpgradePrompt";
import { BreadcrumbIcon } from "./BreadcrumbIcon";
import { LinkButton } from "./Buttons";
import { Header2, Header3 } from "./Headers";
import { Header2 } from "./Headers";
import { LoadingBarDivider } from "./LoadingBarDivider";
import { NamedIcon } from "./NamedIcon";
import { Paragraph } from "./Paragraph";
import { Tabs, TabsProps } from "./Tabs";
import { LoadingBarDivider } from "./LoadingBarDivider";
type WithChildren = {
children: React.ReactNode;
@@ -52,10 +51,10 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
return (
<div className="flex items-center gap-2">
{backButton && (
<div className="group flex items-center gap-2">
<div className="group -ml-1.5 flex items-center gap-0">
<Link
to={backButton.to}
className="text-xs text-text-dimmed transition group-hover:text-text-bright"
className="rounded px-1.5 py-1 text-xs text-text-dimmed transition group-hover:bg-charcoal-700 group-hover:text-text-bright"
>
{backButton.text}
</Link>
@@ -1,6 +1,7 @@
import { cn } from "~/utils/cn";
import { Header2 } from "./Headers";
import { Spinner } from "./Spinner";
import { CheckIcon } from "@heroicons/react/24/solid";
export function StepNumber({
stepNumber,
@@ -21,15 +22,15 @@ export function StepNumber({
<div className={cn("mr-3", className)}>
{active ? (
<div className="flex items-center gap-x-3">
<span className="flex h-6 w-6 items-center justify-center rounded bg-green-600 py-1 text-xs font-semibold text-charcoal-900 shadow">
<span className="flex h-6 w-6 items-center justify-center rounded bg-success py-1 text-xs font-semibold text-charcoal-900">
{stepNumber}
</span>
<Header2>{title}</Header2>
</div>
) : (
<div className="flex items-center gap-x-3">
<span className="flex h-6 w-6 items-center justify-center rounded border border-charcoal-700 bg-charcoal-800 py-1 text-xs font-semibold text-text-dimmed shadow">
{complete ? "✓" : stepNumber}
<span className="flex h-6 w-6 items-center justify-center rounded border border-charcoal-700 bg-charcoal-800 py-1 text-xs font-semibold text-text-dimmed">
{complete ? <CheckIcon className="size-4" /> : stepNumber}
</span>
{displaySpinner ? (
@@ -192,14 +192,9 @@ export class OrganizationsPresenter {
deletedAt: null,
},
},
sources: {
where: {
active: false,
},
},
httpEndpoints: true,
},
},
version: true,
},
orderBy: { name: "asc" },
},
@@ -225,6 +220,7 @@ export class OrganizationsPresenter {
slug: project.slug,
name: project.name,
jobCount: project._count.jobs,
version: project.version,
})),
hasUnconfiguredIntegrations: org._count.integrations > 0,
runsEnabled: org.runsEnabled,
@@ -25,6 +25,7 @@ export class ProjectPresenter {
updatedAt: true,
deletedAt: true,
version: true,
externalRef: true,
_count: {
select: {
sources: {
@@ -65,6 +66,7 @@ export class ProjectPresenter {
return {
id: project.id,
slug: project.slug,
ref: project.externalRef,
name: project.name,
organizationId: project.organizationId,
createdAt: project.createdAt,
@@ -50,6 +50,12 @@ export class RunListPresenter {
const hasStatusFilters =
filterByEnqueuedStatus || (statuses !== undefined && statuses.length > 0);
const hasFilters =
tasks !== undefined ||
versions !== undefined ||
hasStatusFilters ||
environments !== undefined;
// Find the project scoped to the organization
const project = await this.#prismaClient.project.findFirstOrThrow({
select: {
@@ -226,11 +232,12 @@ export class RunListPresenter {
},
};
}),
possibleTasks: possibleTasks.map((task) => task.slug),
pagination: {
next,
previous,
},
possibleTasks: possibleTasks.map((task) => task.slug),
hasFilters,
};
}
}
@@ -1,6 +1,7 @@
import { Link } from "@remix-run/react";
import simplur from "simplur";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { LinkButton } from "~/components/primitives/Buttons";
import { Header3 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
@@ -23,6 +24,9 @@ export default function Page() {
<NavBar>
<PageTitle title={`${organization.title} projects`} />
<PageAccessories>
<Paragraph variant="extra-small" className="text-charcoal-500">
Org UID: {organization.id}
</Paragraph>
<LinkButton
to={newProjectPath(organization)}
variant="primary/small"
@@ -33,13 +37,6 @@ export default function Page() {
</PageAccessories>
</NavBar>
<PageBody>
<PageInfoRow>
<PageInfoGroup alignment="right">
<Paragraph variant="extra-small" className="text-charcoal-600">
UID: {organization.id}
</Paragraph>
</PageInfoGroup>
</PageInfoRow>
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{organization.projects.length > 0 ? (
organization.projects.map((project) => {
@@ -52,7 +49,13 @@ export default function Page() {
<NamedIcon name="folder" className="h-10 w-10 flex-none" />
<div className="flex flex-col">
<Header3>{project.name}</Header3>
<Paragraph variant="small">{simplur`${project.jobCount} Job[|s]`}</Paragraph>
{project.version === "V2" ? (
<Paragraph variant="small">{simplur`${project.jobCount} Job[|s]`}</Paragraph>
) : (
<Badge className="max-w-max" variant="v3">
v3
</Badge>
)}
</div>
</Link>
</li>
@@ -19,6 +19,7 @@ import { useFeatures } from "~/hooks/useFeatures";
import { useOrganization } from "~/hooks/useOrganizations";
import { plansPath, stripePortalPath, usagePath } from "~/utils/pathBuilder";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { Callout } from "~/components/primitives/Callout";
function planLabel(subscription: ActiveSubscription | undefined, periodEnd: Date) {
if (!subscription) {
@@ -47,6 +48,10 @@ export default function Page() {
const { isManagedCloud } = useFeatures();
const currentPlan = useCurrentPlan();
const hasV3Project = organization.projects.some((p) => p.version === "V3");
const hasV2Project = organization.projects.some((p) => p.version === "V2");
const allV3Projects = organization.projects.every((p) => p.version === "V3");
return (
<PageContainer>
<NavBar>
@@ -64,14 +69,16 @@ export default function Page() {
</LinkButton>
</>
)}
<LinkButton
to={plansPath(organization)}
variant="primary/small"
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="px-0"
>
Upgrade
</LinkButton>
{hasV2Project && (
<LinkButton
to={plansPath(organization)}
variant="primary/small"
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="px-0"
>
Upgrade
</LinkButton>
)}
</>
)}
</PageAccessories>
@@ -79,30 +86,44 @@ export default function Page() {
<PageBody scrollable={false}>
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">
<div className="px-4 pt-4">
<PageInfoRow>
<PageInfoGroup>
{currentPlan?.subscription && (
<PageInfoProperty
icon={<ReceiptRefundIcon className="h-4 w-4 text-green-600" />}
value={planLabel(currentPlan.subscription, currentPlan.usage.periodEnd)}
/>
)}
{currentPlan?.subscription?.isPaying && (
<PageInfoProperty
icon={<CalendarDaysIcon className="h-4 w-4 text-green-600" />}
label={"Billing period"}
value={
<>
<DateTime date={currentPlan.usage.periodStart} includeTime={false} /> to{" "}
<DateTime date={currentPlan.usage.periodEnd} includeTime={false} /> (
{formatDurationInDays(currentPlan.usage.periodRemainingDuration)} remaining)
</>
}
/>
)}
</PageInfoGroup>
</PageInfoRow>
{isManagedCloud && (
{allV3Projects ? (
<Callout variant="warning" className="mb-3">
This organization only has v3 projects. These are currently free. Usage data and
billing will be available soon.
</Callout>
) : hasV3Project ? (
<Callout variant="warning" className="mb-3">
This organization has a mix of v2 and v3 projects v3 projects do not count towards
your usage (below) and are currently free.
</Callout>
) : null}
{hasV2Project && (
<PageInfoRow>
<PageInfoGroup>
{currentPlan?.subscription && (
<PageInfoProperty
icon={<ReceiptRefundIcon className="h-4 w-4 text-green-600" />}
value={planLabel(currentPlan.subscription, currentPlan.usage.periodEnd)}
/>
)}
{currentPlan?.subscription?.isPaying && (
<PageInfoProperty
icon={<CalendarDaysIcon className="h-4 w-4 text-green-600" />}
label={"Billing period"}
value={
<>
<DateTime date={currentPlan.usage.periodStart} includeTime={false} /> to{" "}
<DateTime date={currentPlan.usage.periodEnd} includeTime={false} /> (
{formatDurationInDays(currentPlan.usage.periodRemainingDuration)}{" "}
remaining)
</>
}
/>
)}
</PageInfoGroup>
</PageInfoRow>
)}
{hasV2Project && isManagedCloud && (
<PageTabs
tabs={[
{
@@ -118,9 +139,11 @@ export default function Page() {
/>
)}
</div>
<div className="overflow-y-auto pb-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<Outlet />
</div>
{hasV2Project && (
<div className="overflow-y-auto pb-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<Outlet />
</div>
)}
</div>
</PageBody>
</PageContainer>
@@ -1,11 +1,20 @@
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { TaskRunAttemptStatus } from "@trigger.dev/database";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { Feedback } from "~/components/Feedback";
import { InitCommandV3, TriggerDevStepV3 } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import { Header1 } from "~/components/primitives/Headers";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import {
Table,
TableBlankRow,
@@ -18,6 +27,8 @@ import {
} from "~/components/primitives/Table";
import { TaskPath } from "~/components/runs/v3/TaskPath";
import { TaskRunStatus } from "~/components/runs/v3/TaskRunStatus";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
@@ -137,7 +148,7 @@ export default function Page() {
</Table>
</div>
) : (
<Paragraph>You have no background workers yet</Paragraph>
<CreateTaskInstructions />
)}
</div>
</div>
@@ -149,8 +160,47 @@ export default function Page() {
function classForTaskAttemptStatus(status: TaskRunAttemptStatus) {
switch (status) {
case "FAILED":
return "text-red-500";
return "text-error";
default:
return "";
}
}
function CreateTaskInstructions() {
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
return (
<MainCenteredContainer className="max-w-prose">
<div className="mb-6 flex items-center justify-between border-b">
<Header1 spacing>Get setup in 3 minutes</Header1>
<div className="flex items-center gap-2">
<Feedback
button={
<Button variant="minimal/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<StepNumber stepNumber="1" title="Run the CLI 'init' command in your project" />
<StepContentContainer>
<InitCommandV3 />
<Paragraph spacing>
Youll notice a new folder in your project called{" "}
<InlineCode variant="small">trigger</InlineCode>. Weve added a very simple example task
in here to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStepV3 />
</StepContentContainer>
<StepNumber stepNumber="3" title="Waiting for tasks" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</MainCenteredContainer>
);
}
@@ -123,25 +123,14 @@ export default function Page() {
</PropertyTable>
{event.events !== undefined && <SpanEvents spanEvents={event.events} />}
{event.payload && (
<div>
<Header2 spacing>Payload</Header2>
<CodeBlock code={event.payload} maxLines={20} />
</div>
{event.payload !== undefined && (
<CodeBlock rowTitle="Payload" code={event.payload} maxLines={20} />
)}
{event.output && (
<div>
<Header2 spacing>Output</Header2>
<CodeBlock code={event.output} maxLines={20} />
</div>
{event.output !== undefined && (
<CodeBlock rowTitle="Output" code={event.output} maxLines={20} />
)}
{event.properties !== undefined && (
<div>
<Header2 spacing>Properties</Header2>
<CodeBlock code={event.properties} maxLines={20} />
</div>
<CodeBlock rowTitle="Properties" code={event.properties} maxLines={20} />
)}
</div>
</div>
@@ -1,17 +1,27 @@
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
import { useNavigation } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { StepContentContainer } from "~/components/StepContentContainer";
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Header1 } from "~/components/primitives/Headers";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { RunsFilters, TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { RunListPresenter } from "~/presenters/v3/RunListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { ProjectParamSchema } from "~/utils/pathBuilder";
import { ProjectParamSchema, v3ProjectPath, v3TestPath } from "~/utils/pathBuilder";
import { ListPagination } from "../../components/ListPagination";
import { PageBody } from "~/components/layout/AppLayout";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { TextLink } from "~/components/primitives/TextLink";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -54,29 +64,104 @@ export default function Page() {
<PageTitle title="Runs" />
</NavBar>
<PageBody>
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
<div>
<div className="mb-2 flex items-center justify-between gap-x-2">
<RunsFilters
possibleEnvironments={project.environments}
possibleTasks={list.possibleTasks}
/>
<div className="flex items-center justify-end gap-x-2">
<ListPagination list={list} />
{list.possibleTasks.length === 0 ? (
<CreateFirstTaskInstructions />
) : list.runs.length === 0 && !list.hasFilters ? (
<RunTaskInstructions />
) : (
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
<div>
<div className="mb-2 flex items-center justify-between gap-x-2">
<RunsFilters
possibleEnvironments={project.environments}
possibleTasks={list.possibleTasks}
/>
<div className="flex items-center justify-end gap-x-2">
<ListPagination list={list} />
</div>
</div>
</div>
<TaskRunsTable
total={list.runs.length}
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
currentUser={user}
/>
<ListPagination list={list} className="mt-2 justify-end" />
<TaskRunsTable
total={list.runs.length}
hasFilters={list.hasFilters}
runs={list.runs}
isLoading={isLoading}
currentUser={user}
/>
<ListPagination list={list} className="mt-2 justify-end" />
</div>
</div>
</div>
)}
</PageBody>
</>
);
}
function CreateFirstTaskInstructions() {
const organization = useOrganization();
const project = useProject();
return (
<MainCenteredContainer className="max-w-prose">
<BlankstateInstructions title="Create your first task">
<Paragraph spacing>
Before running a task, you must first create one. Follow the instructions on the{" "}
<TextLink to={v3ProjectPath(organization, project)}>Tasks</TextLink> page to create a
task, then return here to run it.
</Paragraph>
<LinkButton
to={v3ProjectPath(organization, project)}
variant="primary/medium"
LeadingIcon={TaskIcon}
className="inline-flex"
>
Create your first task
</LinkButton>
</BlankstateInstructions>
</MainCenteredContainer>
);
}
function RunTaskInstructions() {
const organization = useOrganization();
const project = useProject();
return (
<MainCenteredContainer className="max-w-prose">
<Header1 className="mb-6 border-b py-2">How to run your tasks</Header1>
<StepNumber stepNumber="A" title="Trigger a test run" />
<StepContentContainer>
<Paragraph spacing>
You can perform a Run with any payload you want, or use one of our examples on the test
page.
</Paragraph>
<LinkButton
to={v3TestPath(organization, project)}
variant="primary/medium"
LeadingIcon={BeakerIcon}
className="inline-flex"
>
Test
</LinkButton>
<div className="mt-6 flex items-center gap-2">
<hr className="w-full" />
<Paragraph variant="extra-extra-small/dimmed/caps">OR</Paragraph>
<hr className="w-full" />
</div>
</StepContentContainer>
<StepNumber stepNumber="B" title="Trigger your task for real" />
<StepContentContainer>
<Paragraph spacing>
Performing a real run depends on the type of Trigger your Task is using.
</Paragraph>
<LinkButton
to="https://trigger.dev/docs"
variant="primary/medium"
LeadingIcon={BookOpenIcon}
className="inline-flex"
>
How to run a task
</LinkButton>
</StepContentContainer>
</MainCenteredContainer>
);
}
@@ -0,0 +1,169 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { ActionFunction, json } from "@remix-run/server-runtime";
import { redirect } from "remix-typedjson";
import { z } from "zod";
import { InlineCode } from "~/components/code/InlineCode";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { prisma } from "~/db.server";
import { useProject } from "~/hooks/useProject";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import {
clearCurrentProjectId,
commitCurrentProjectSession,
} from "~/services/currentProject.server";
import { DeleteProjectService } from "~/services/deleteProject.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath, projectPath, v3ProjectPath } from "~/utils/pathBuilder";
export function createSchema(
constraints: {
getSlugMatch?: (slug: string) => { isMatch: boolean; projectSlug: string };
} = {}
) {
return z.discriminatedUnion("action", [
z.object({
action: z.literal("rename"),
projectName: z.string().min(3, "Project name must have at least 3 characters").max(50),
}),
]);
}
export const action: ActionFunction = async ({ request, params }) => {
const userId = await requireUserId(request);
const { organizationSlug, projectParam } = params;
if (!organizationSlug || !projectParam) {
return json({ errors: { body: "organizationSlug is required" } }, { status: 400 });
}
const formData = await request.formData();
const schema = createSchema({
getSlugMatch: (slug) => {
return { isMatch: slug === projectParam, projectSlug: projectParam };
},
});
const submission = parse(formData, { schema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
switch (submission.value.action) {
case "rename": {
await prisma.project.update({
where: {
slug: projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
data: {
name: submission.value.projectName,
},
});
return redirectWithSuccessMessage(
v3ProjectPath({ slug: organizationSlug }, { slug: projectParam }),
request,
`Project renamed to ${submission.value.projectName}`
);
}
}
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
};
export default function Page() {
const project = useProject();
const lastSubmission = useActionData();
const navigation = useNavigation();
const [renameForm, { projectName }] = useForm({
id: "rename-project",
// TODO: type this
lastSubmission: lastSubmission as any,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, {
schema: createSchema(),
});
},
});
const isRenameLoading =
navigation.formData?.get("action") === "rename" &&
(navigation.state === "submitting" || navigation.state === "loading");
return (
<PageContainer>
<NavBar>
<PageTitle title={`${project.name} project settings`} />
</NavBar>
<PageBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<Fieldset>
<InputGroup>
<Label>Project ref</Label>
<ClipboardField value={`proj_${project.ref}`} variant={"secondary/small"} />
<Hint>
This goes in your <InlineCode>trigger.config</InlineCode> file.
</Hint>
</InputGroup>
</Fieldset>
<Form method="post" {...renameForm.props} className="max-w-md">
<input type="hidden" name="action" value="rename" />
<Fieldset>
<InputGroup>
<Label htmlFor={projectName.id}>Rename your project</Label>
<Input
{...conform.input(projectName, { type: "text" })}
defaultValue={project.name}
placeholder="Your project name"
icon="folder"
autoFocus
/>
<FormError id={projectName.errorId}>{projectName.error}</FormError>
</InputGroup>
<FormButtons
confirmButton={
<Button
type="submit"
variant={"primary/small"}
disabled={isRenameLoading}
LeadingIcon={isRenameLoading ? "spinner-white" : undefined}
>
Rename project
</Button>
}
/>
</Fieldset>
</Form>
</div>
</div>
</PageBody>
</PageContainer>
);
}
@@ -1,24 +1,28 @@
import { NavLink, Outlet, useNavigation, useParams } from "@remix-run/react";
import { NavLink, Outlet, useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Header2 } from "~/components/primitives/Headers";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RadioButtonCircle } from "~/components/primitives/RadioButton";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "~/components/primitives/Resizable";
import { TextLink } from "~/components/primitives/TextLink";
import { TaskPath } from "~/components/runs/v3/TaskPath";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { TaskListItem, TestPresenter } from "~/presenters/v3/TestPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { ProjectParamSchema, v3TestTaskPath } from "~/utils/pathBuilder";
import { ProjectParamSchema, v3ProjectPath, v3TestTaskPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -49,10 +53,16 @@ export default function Page() {
<ResizablePanelGroup direction="horizontal" className="h-full max-h-full">
<ResizablePanel order={1} minSize={20} defaultSize={30}>
<div className="flex flex-col px-3">
<div className="flex h-10 items-center border-b border-grid-dimmed">
<Header2>Select a task</Header2>
</div>
<TaskSelector tasks={tasks} />
{tasks.length === 0 ? (
<NoTaskInstructions />
) : (
<>
<div className="flex h-10 items-center border-b border-grid-dimmed">
<Header2>Select a task</Header2>
</div>
<TaskSelector tasks={tasks} />
</>
)}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
@@ -97,3 +107,25 @@ function TaskSelector({ tasks }: { tasks: TaskListItem[] }) {
</div>
);
}
function NoTaskInstructions() {
const organization = useOrganization();
const project = useProject();
return (
<BlankstateInstructions title="Create your first task">
<Paragraph spacing variant="small">
Before testing, you must first create a task. Follow the instructions on the{" "}
<TextLink to={v3ProjectPath(organization, project)}>Tasks</TextLink> page to create a task
then return here to run a test.
</Paragraph>
<LinkButton
to={v3ProjectPath(organization, project)}
variant="primary/small"
LeadingIcon={TaskIcon}
className="inline-flex"
>
Create your first task
</LinkButton>
</BlankstateInstructions>
);
}
@@ -78,7 +78,7 @@ export const action: ActionFunction = async ({ request }) => {
"Set-Cookie": await commitCurrentProjectSession(session),
};
if (isManagedCloud) {
if (isManagedCloud && submission.value.projectVersion === "v2") {
return redirect(selectPlanPath(organization), {
headers,
});
@@ -7,7 +7,12 @@ import {
NoSymbolIcon,
} from "@heroicons/react/20/solid";
import { EnvelopeIcon } from "@heroicons/react/24/solid";
import { AirtableIcon, GitHubDarkIcon, SlackIcon } from "@trigger.dev/companyicons";
import {
AirtableIcon,
GitHubDarkIcon,
GitHubLightIcon,
SlackIcon,
} from "@trigger.dev/companyicons";
import { Button } from "~/components/primitives/Buttons";
import { Header1, Header3 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
@@ -269,7 +274,7 @@ export default function Story() {
<div className="flex flex-col gap-2">
<div className="flex flex-col items-start gap-2">
<Button variant="primary/large" fullWidth>
<GitHubDarkIcon className={"mr-1.5 h-4 w-4"} />
<GitHubDarkIcon className={"mr-1.5 size-[1.2rem]"} />
<span className="text-charcoal-900">Continue with GitHub</span>
</Button>
<Button variant="secondary/large" fullWidth>
@@ -278,6 +283,10 @@ export default function Story() {
/>
<span className="text-primary group-hover:text-apple-200">Continue with Email</span>
</Button>
<Button variant="tertiary/large" fullWidth>
<GitHubLightIcon className={"mr-1.5 size-[1.2rem]"} />
<span className="text-text-bright">Continue with GitHub</span>
</Button>
<Button variant="danger/large" fullWidth>
<NamedIcon
name={"trash-can"}
@@ -304,6 +313,10 @@ export default function Story() {
/>
<span className="text-primary group-hover:text-apple-200">Continue with Email</span>
</Button>
<Button variant="tertiary/extra-large" fullWidth>
<GitHubLightIcon className={"mr-1.5 h-5 w-5"} />
<span className="text-text-bright">Continue with GitHub</span>
</Button>
<Button variant="danger/extra-large" fullWidth>
<NamedIcon
name={"trash-can"}
@@ -5,21 +5,31 @@ import { Paragraph } from "~/components/primitives/Paragraph";
export default function Story() {
return (
<div className="flex gap-8">
<div className="flex flex-col items-start gap-y-[2.58rem] whitespace-nowrap p-8">
<Header2>Clipboards</Header2>
<Paragraph variant="small">primary/small</Paragraph>
<Paragraph variant="small">secondary/small</Paragraph>
<Paragraph variant="small">tertiary/small</Paragraph>
<Paragraph variant="small">tertiary/small + LeadingIcon</Paragraph>
<Paragraph variant="small">tertiary/small + LeadingIcon</Paragraph>
<Paragraph variant="small">primary/medium</Paragraph>
<Paragraph variant="small">secondary/medium</Paragraph>
<Paragraph variant="small">tertiary/medium</Paragraph>
<Paragraph variant="small">tertiary/medium + LeadingIcon</Paragraph>
<Paragraph variant="small">tertiary/medium + LeadingIcon</Paragraph>
<div className="flex gap-x-8 p-8">
<div className="flex flex-col items-start whitespace-nowrap">
<Header2 className="mb-[2.2rem]">Variants</Header2>
<div className="mb-10 space-y-[2.33rem]">
<Paragraph variant="small/bright">primary/small</Paragraph>
<Paragraph variant="small/bright">secondary/small</Paragraph>
<Paragraph variant="small/bright">tertiary/small</Paragraph>
<Paragraph variant="small/bright">tertiary/small + LeadingIcon</Paragraph>
<Paragraph variant="small/bright">tertiary/small + LeadingIcon</Paragraph>
<Paragraph variant="small/bright">primary/small + iconButton</Paragraph>
<Paragraph variant="small/bright">secondary/small + iconButton</Paragraph>
<Paragraph variant="small/bright">tertiary/small + iconButton</Paragraph>
</div>
<div className="space-y-[2.9rem]">
<Paragraph variant="small/bright">primary/medium</Paragraph>
<Paragraph variant="small/bright">secondary/medium</Paragraph>
<Paragraph variant="small/bright">tertiary/medium</Paragraph>
<Paragraph variant="small/bright">tertiary/medium + LeadingIcon</Paragraph>
<Paragraph variant="small/bright">tertiary/medium + LeadingIcon</Paragraph>
<Paragraph variant="small/bright">primary/medium + iconButton</Paragraph>
<Paragraph variant="small/bright">secondary/medium + iconButton</Paragraph>
<Paragraph variant="small/bright">tertiary/medium + iconButton</Paragraph>
</div>
</div>
<div className="flex flex-col items-start gap-y-8 p-8">
<div className="flex flex-col items-start gap-y-8">
<Header2>Default</Header2>
<ClipboardField value="copy paste me" variant="primary/small" />
<ClipboardField value="copy paste me" variant="secondary/small" />
@@ -27,9 +37,12 @@ export default function Story() {
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
icon={<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />}
/>
<ClipboardField value="with leadingIcon" variant="tertiary/small" icon="search" />
<ClipboardField value="with iconButton" variant="primary/small" iconButton />
<ClipboardField value="with iconButton" variant="secondary/small" iconButton />
<ClipboardField value="with iconButton" variant="tertiary/small" iconButton />
<ClipboardField value="copy paste me" variant="primary/medium" />
<ClipboardField value="copy paste me" variant="secondary/medium" />
<ClipboardField value="copy paste me" variant="tertiary/medium" />
@@ -39,8 +52,11 @@ export default function Story() {
icon={<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />}
/>
<ClipboardField value="with leadingIcon" variant="tertiary/medium" icon="search" />
<ClipboardField value="with iconButton" variant="primary/medium" iconButton />
<ClipboardField value="with iconButton" variant="secondary/medium" iconButton />
<ClipboardField value="with iconButton" variant="tertiary/medium" iconButton />
</div>
<div className="flex flex-col items-start gap-y-8 p-8">
<div className="flex flex-col items-start gap-y-8">
<Header2>Secure value</Header2>
<ClipboardField value="copy paste me" variant="primary/small" secure={true} />
<ClipboardField value="copy paste me" variant="secondary/small" secure={true} />
@@ -48,8 +64,8 @@ export default function Story() {
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
secure={true}
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
/>
<ClipboardField
value="with leadingIcon"
@@ -57,13 +73,22 @@ export default function Story() {
icon="search"
secure={true}
/>
<ClipboardField value="with iconButton" variant="primary/small" iconButton secure={true} />
<ClipboardField
value="with iconButton"
variant="secondary/small"
iconButton
secure={true}
/>
<ClipboardField value="with iconButton" variant="tertiary/small" iconButton secure={true} />
{/* Medium size */}
<ClipboardField value="copy paste me" variant="primary/medium" secure={true} />
<ClipboardField value="copy paste me" variant="secondary/medium" secure={true} />
<ClipboardField value="copy paste me" variant="tertiary/medium" secure={true} />
<ClipboardField
value="with leadingIcon"
variant="tertiary/medium"
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
secure={true}
/>
<ClipboardField
@@ -72,8 +97,21 @@ export default function Story() {
icon="search"
secure={true}
/>
<ClipboardField value="with iconButton" variant="primary/medium" iconButton secure={true} />
<ClipboardField
value="with iconButton"
variant="secondary/medium"
iconButton
secure={true}
/>
<ClipboardField
value="with iconButton"
variant="tertiary/medium"
iconButton
secure={true}
/>
</div>
<div className="flex flex-col items-start gap-y-8 p-8">
<div className="flex flex-col items-start gap-y-8">
<Header2>Custom secure value</Header2>
<ClipboardField value="npx abcdefghi" variant="primary/small" secure="npx ••••••••" />
<ClipboardField value="npx abcdefghi" variant="secondary/small" secure="npx ••••••••" />
@@ -81,7 +119,7 @@ export default function Story() {
<ClipboardField
value="npx abcdefghi"
variant="tertiary/small"
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
secure="npx ••••••••"
/>
<ClipboardField
@@ -90,6 +128,25 @@ export default function Story() {
icon="search"
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="primary/small"
iconButton
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="secondary/small"
iconButton
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="tertiary/small"
iconButton
secure="npx ••••••••"
/>
{/* Medium size */}
<ClipboardField value="npx abcdefghi" variant="primary/medium" secure="npx ••••••••" />
<ClipboardField value="npx abcdefghi" variant="secondary/medium" secure="npx ••••••••" />
<ClipboardField value="npx abcdefghi" variant="tertiary/medium" secure="npx ••••••••" />
@@ -105,6 +162,24 @@ export default function Story() {
icon="search"
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="primary/medium"
iconButton
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="secondary/medium"
iconButton
secure="npx ••••••••"
/>
<ClipboardField
value="npx abcdefghi"
variant="tertiary/medium"
iconButton
secure="npx ••••••••"
/>
</div>
</div>
);
@@ -1,39 +1,100 @@
import { CodeBlock } from "~/components/code/CodeBlock";
import { Header2 } from "~/components/primitives/Headers";
export default function Story() {
return (
<div className="flex flex-col items-start gap-y-8 p-8">
<CodeBlock
code={`{ id: "my-first-job" }`}
highlightedRanges={[
[6, 8],
[12, 14],
]}
/>
<CodeBlock
code={`export const client = new TriggerClient("smoke-test", {
apiUrl: "http://localhost:3000",
endpoint: "http://localhost:3007/__trigger/entry",
logLevel: "debug",
longLine: "This is a long line that will scroll off the edge of the screen and cause a horizontal scrollbar",
onLog: (log) => {
console.log(log);
},
onLogError: (log) => {
console.error(log);
},
onLogWarning: (log) => {
console.warn(log);
},
onLogInfo: (log) => {
console.info(log);
},
});`}
highlightedRanges={[
[6, 8],
[12, 14],
]}
/>
<div>
<Header2>Inline field</Header2>
<CodeBlock code={`{ id: "my-first-job" }`} />
</div>
<div>
<Header2 className="mb-2">With title row</Header2>
<CodeBlock
rowTitle="Trigger client"
code={`export const client = new TriggerClient("smoke-test", {
apiUrl: "http://localhost:3000",
endpoint: "http://localhost:3007/__trigger/entry",
logLevel: "debug",
longLine: "This is a long line that will scroll off the edge of the screen and cause a horizontal scrollbar",
});`}
/>
</div>
<div>
<Header2 className="mb-2">showChrome</Header2>
<CodeBlock
showChrome
fileName="trigger-client.ts"
code={`export const client = new TriggerClient("smoke-test", {
apiUrl: "http://localhost:3000",
endpoint: "http://localhost:3007/__trigger/entry",
logLevel: "debug",
longLine: "This is a long line that will scroll off the edge of the screen and cause a horizontal scrollbar",
onLog: (log) => {
console.log(log);
},
onLogError: (log) => {
console.error(log);
},
onLogWarning: (log) => {
console.warn(log);
},
onLogInfo: (log) => {
console.info(log);
},
});`}
/>
</div>
<div>
<Header2 className="mb-2">Inline copy button</Header2>
<CodeBlock
code={`export const client = new TriggerClient("smoke-test", {
apiUrl: "http://localhost:3000",
endpoint: "http://localhost:3007/__trigger/entry",
logLevel: "debug",
longLine: "This is a long line that will scroll off the edge of the screen and cause a horizontal scrollbar",
onLog: (log) => {
console.log(log);
},
onLogError: (log) => {
console.error(log);
},
onLogWarning: (log) => {
console.warn(log);
},
onLogInfo: (log) => {
console.info(log);
},
});`}
/>
</div>
<div>
<Header2 className="mb-2">Highlighted range</Header2>
<CodeBlock
code={`export const client = new TriggerClient("smoke-test", {
apiUrl: "http://localhost:3000",
endpoint: "http://localhost:3007/__trigger/entry",
logLevel: "debug",
longLine: "This is a long line that will scroll off the edge of the screen and cause a horizontal scrollbar",
onLog: (log) => {
console.log(log);
},
onLogError: (log) => {
console.error(log);
},
onLogWarning: (log) => {
console.warn(log);
},
onLogInfo: (log) => {
console.info(log);
},
});`}
highlightedRanges={[
[6, 8],
[12, 14],
]}
/>
</div>
</div>
);
}
@@ -15,11 +15,11 @@ export default function Story() {
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account and remove
your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogDescription>
This action cannot be undone. This will permanently delete your account and remove your
data from our servers.
</DialogDescription>
</DialogContent>
</Dialog>
</div>
@@ -1,28 +1,19 @@
import { FreePlanUsage } from "~/components/billing/FreePlanUsage";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { organizationBillingPath } from "~/utils/pathBuilder";
const mockOrganization: MatchedOrganization = {
id: "mockID",
title: "mockTitle",
slug: "mockSlug",
projects: [
{ id: "mockId1", slug: "mockSlug1", name: "mockName1", jobCount: 1 },
{ id: "mockId2", slug: "mockSlug2", name: "mockName2", jobCount: 2 },
],
hasUnconfiguredIntegrations: false,
runsEnabled: true,
};
type FreePlanUsageBarProps = {
organization: MatchedOrganization;
};
export default function Story({ organization = mockOrganization }: FreePlanUsageBarProps) {
export default function Story() {
return (
<div className="flex h-screen flex-col items-center justify-center p-12">
<div className="flex h-screen flex-col items-center justify-center gap-4 p-12">
<div className="w-fit">
<FreePlanUsage to={organizationBillingPath(organization)} percentage={0.75} />
<FreePlanUsage to={""} percentage={0.1} />
</div>
<div className="w-fit">
<FreePlanUsage to={""} percentage={0.5} />
</div>
<div className="w-fit">
<FreePlanUsage to={""} percentage={0.75} />
</div>
<div className="w-fit">
<FreePlanUsage to={""} percentage={1} />
</div>
</div>
);
@@ -4,7 +4,7 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
export default function Story() {
return (
<div className="flex flex-col gap-4 bg-charcoal-800 p-4">
<div className="bg-[#0B1018]">
<div className="bg-background-bright">
<NavBar>
<PageTitle title="Organizations" />
<PageAccessories>
@@ -14,7 +14,7 @@ export default function Story() {
</PageAccessories>
</NavBar>
</div>
<div className="bg-[#0B1018]">
<div className="bg-background-bright">
<NavBar>
<PageTitle title="Your Organizations" backButton={{ to: "#", text: "Orgs" }} />
<PageAccessories>
+4
View File
@@ -350,6 +350,10 @@ export function v3RunStreamingPath(
return `${v3RunPath(organization, project, run)}/stream`;
}
export function v3ProjectSettingsPath(organization: OrgForPath, project: ProjectForPath) {
return `${v3ProjectPath(organization, project)}/settings`;
}
// Integration
export function integrationClientPath(organization: OrgForPath, client: IntegrationForPath) {
return `${organizationIntegrationsPath(organization)}/${clientParam(client)}`;