Merge branch 'main' into v3/infra-updates

This commit is contained in:
nicktrn
2024-03-20 13:20:51 +00:00
29 changed files with 463 additions and 215 deletions
+80 -20
View File
@@ -2,10 +2,10 @@ import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons";
import { ReactNode, useState } from "react";
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { Button } from "./primitives/Buttons";
import { Button, LinkButton } from "./primitives/Buttons";
import { Fieldset } from "./primitives/Fieldset";
import { FormButtons } from "./primitives/FormButtons";
import { FormError } from "./primitives/FormError";
@@ -21,8 +21,12 @@ import {
SelectTrigger,
SelectValue,
} from "./primitives/Select";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
import { cn } from "~/utils/cn";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ActivityIcon, HeartPulseIcon } from "lucide-react";
import { docsPath } from "~/utils/pathBuilder";
type FeedbackProps = {
button: ReactNode;
@@ -56,12 +60,22 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild={true}>{button}</SheetTrigger>
<SheetContent>
<SheetContent className="@container">
<SheetBody className="flex h-full flex-col justify-between">
<DiscordBanner />
<Header2 className="mb-4">How can we help?</Header2>
<Header2 className="mb-2.5 text-xl">Get help from the community</Header2>
<Paragraph className="mb-4">
The quickest way to get help and feedback or to provide advice to others is to join our
Discord.
</Paragraph>
<div className="flex flex-col gap-x-4 @[30rem]:flex-row">
<DiscordBanner />
<GitHubDiscussionsBanner />
</div>
<hr className="mb-4" />
<Header2 className="mb-2.5 text-xl">Send us an email</Header2>
<Paragraph className="mb-4">We read every message and respond quickly.</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset className="max-w-full">
<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup className="max-w-full">
<SelectGroup>
@@ -86,46 +100,92 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<div className="flex w-full items-start justify-between">
<Paragraph variant="small" className="w-full">
We read every message and respond quickly.
</Paragraph>
<div className="flex w-full justify-end">
<FormButtons
className="m-0 w-max"
confirmButton={
<Button type="submit" variant="primary/small">
Send
<Button type="submit" variant="tertiary/medium">
Send message
</Button>
}
/>
</div>
</Fieldset>
</Form>
<hr className="my-4" />
<Header2 className="mb-2.5 text-xl">Troubleshooting</Header2>
<Paragraph className="mb-4">
If you're having trouble, check out our documentation or the Trigger.dev Status page.
</Paragraph>
<div className="flex flex-wrap gap-2">
<LinkButton to={docsPath("")} variant="tertiary/medium" LeadingIcon={BookOpenIcon}>
Docs
</LinkButton>
<LinkButton
to={docsPath("v3/introduction")}
variant="tertiary/medium"
LeadingIcon={BookOpenIcon}
>
v3 Docs (Developer preview)
</LinkButton>
<LinkButton
to={"https://trigger.openstatus.dev/"}
variant="tertiary/medium"
LeadingIcon={ActivityIcon}
>
Trigger.dev Status
</LinkButton>
</div>
</SheetBody>
</SheetContent>
</Sheet>
);
}
function DiscordBanner() {
function DiscordBanner({ className }: { className?: string }) {
return (
<a
href="https://trigger.dev/discord"
target="_blank"
className="group mb-4 flex w-full items-center justify-between rounded-md border border-grid-bright p-4 transition hover:border-text-link"
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",
className
)}
>
<div className="flex flex-col gap-y-2">
<DiscordIcon className="h-8 w-8" />
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
Join the Trigger.dev
<br />
Discord community
Join our Discord community
</Header1>
<Paragraph variant="small">
<Paragraph variant="small" className="mb-4">
Get help or answer questions from the Trigger.dev community.
</Paragraph>
</div>
<ChevronRightIcon className="size-5 text-grid-bright transition group-hover:translate-x-1 group-hover:text-text-link" />
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-link" />
</a>
);
}
function GitHubDiscussionsBanner({ className }: { className?: string }) {
return (
<a
href="https://github.com/triggerdotdev/trigger.dev/discussions"
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-dimmed",
className
)}
>
<div className="flex flex-col gap-y-2">
<GitHubLightIcon className="mb-1 h-7 w-7" />
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
View our GitHub Discussions
</Header1>
<Paragraph variant="small">
Post your questions, feedback, and feature requests on GitHub.
</Paragraph>
</div>
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-bright" />
</a>
);
}
@@ -14,6 +14,7 @@ import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
import { useNavigation } from "@remix-run/react";
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
import { Fragment, useEffect, useRef, useState } from "react";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { useFeatures } from "~/hooks/useFeatures";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { MatchedProject } from "~/hooks/useProject";
@@ -54,6 +55,7 @@ import { LogoIcon } from "../LogoIcon";
import { StepContentContainer } from "../StepContentContainer";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { FreePlanUsage } from "../billing/FreePlanUsage";
import { Badge } from "../primitives/Badge";
import { Button } from "../primitives/Buttons";
import { ClipboardField } from "../primitives/ClipboardField";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
@@ -70,8 +72,6 @@ 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<
@@ -269,7 +269,6 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
target="_blank"
/>
)}
<SideMenuItem
name="Changelog"
icon="star"
@@ -277,20 +276,37 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
data-action="changelog"
target="_blank"
/>
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
data-action="help & feedback"
fullWidth
textAlignLeft
>
Help & Feedback
</Button>
}
/>
{project.version === "V2" ? (
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
data-action="help & feedback"
fullWidth
textAlignLeft
>
Help & Feedback
</Button>
}
/>
) : (
<Feedback
defaultValue="developer preview"
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
leadingIconClassName="text-primary"
data-action="help & feedback"
fullWidth
textAlignLeft
>
<span className="text-primary">Give feedback on v3</span>
</Button>
}
/>
)}
{currentPlan && !currentPlan.subscription?.isPaying && currentPlan.usage.runCountCap && (
<FreePlanUsage
to={organizationBillingPath(organization)}
@@ -1,6 +1,7 @@
import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
import { Link, useNavigation } from "@remix-run/react";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { plansPath } from "~/utils/pathBuilder";
import { UpgradePrompt, useShowUpgradePrompt } from "../billing/UpgradePrompt";
@@ -21,6 +22,7 @@ export function NavBar({ children }: WithChildren) {
const showUpgradePrompt = useShowUpgradePrompt(organization);
const navigation = useNavigation();
const isLoading = navigation.state === "loading" || navigation.state === "submitting";
const project = useOptionalProject();
return (
<div>
@@ -66,7 +66,7 @@ const SelectContent = React.forwardRef<
>
<SelectPrimitive.Viewport
className={cn(
"px-1 py-0",
"space-y-0.5 px-1 py-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
@@ -93,27 +93,30 @@ const SelectLabel = React.forwardRef<
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative my-0.5 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition first-of-type:my-1 last-of-type:my-1 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
contentClassName?: string;
};
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
({ className, children, contentClassName, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
@@ -4,7 +4,7 @@ import { cn } from "~/utils/cn";
const variantClasses = {
basic:
"bg-background-dimmed border border-charcoal-800 rounded-md px-3 py-1.5 text-sm text-text-bright shadow-md fade-in-50",
"bg-background-dimmed border border-charcoal-700 rounded-md px-3 py-1.5 text-sm text-text-bright shadow-md fade-in-50",
dark: "bg-background-dimmed border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50",
};
@@ -55,6 +55,7 @@ function SimpleTooltip({
side,
hidden,
variant,
disableHoverableContent = false,
className,
}: {
button: React.ReactNode;
@@ -62,10 +63,11 @@ function SimpleTooltip({
side?: React.ComponentProps<typeof TooltipContent>["side"];
hidden?: boolean;
variant?: Variant;
disableHoverableContent?: boolean;
className?: string;
}) {
return (
<TooltipProvider>
<TooltipProvider disableHoverableContent={disableHoverableContent}>
<Tooltip>
<TooltipTrigger className="h-fit">{button}</TooltipTrigger>
<TooltipContent
@@ -81,34 +83,4 @@ function SimpleTooltip({
);
}
export function LoginTooltip({
children,
side,
content,
className,
}: {
children: React.ReactNode;
side: "top" | "bottom" | "left" | "right";
content: React.ReactNode | string;
className?: string;
}) {
return (
<TooltipProvider delayDuration={2500} disableHoverableContent>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className={cn(
"max-w-xs border-charcoal-800 bg-charcoal-900 px-5 py-4 backdrop-blur-md",
className
)}
side={side}
sideOffset={14}
>
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipArrow, SimpleTooltip };
@@ -1,16 +1,10 @@
import {
ArrowPathIcon,
BoltSlashIcon,
BugAntIcon,
CheckCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
NoSymbolIcon,
PauseCircleIcon,
RectangleStackIcon,
ServerStackIcon,
XCircleIcon,
} from "@heroicons/react/20/solid";
import { TaskRunStatus, WorkerDeploymentStatus } from "@trigger.dev/database";
import { WorkerDeploymentStatus } from "@trigger.dev/database";
import { Spinner } from "~/components/primitives/Spinner";
import { cn } from "~/utils/cn";
@@ -53,6 +47,12 @@ export function DeploymentStatusIcon({
return <NoSymbolIcon className={cn(deploymentStatusClassNameColor(status), className)} />;
case "FAILED":
return <XCircleIcon className={cn(deploymentStatusClassNameColor(status), className)} />;
case "TIMED_OUT":
return (
<ExclamationTriangleIcon
className={cn(deploymentStatusClassNameColor(status), className)}
/>
);
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
@@ -66,6 +66,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
case "BUILDING":
case "DEPLOYING":
return "text-pending";
case "TIMED_OUT":
case "CANCELED":
return "text-charcoal-500";
case "DEPLOYED":
@@ -91,6 +92,8 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
return "Deployed";
case "CANCELED":
return "Canceled";
case "TIMED_OUT":
return "Timed out";
case "FAILED":
return "Failed";
default: {
@@ -1,9 +1,15 @@
import { TrashIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import type { TaskRunStatus as TaskRunStatusType } from "@trigger.dev/database";
import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import { useCallback } from "react";
import { z } from "zod";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
import { Button } from "../../primitives/Buttons";
@@ -17,9 +23,20 @@ import {
SelectValue,
} from "../../primitives/Select";
import { TimeFrameFilter } from "../TimeFrameFilter";
import { TaskRunStatusCombo } from "./TaskRunStatus";
import { TaskRunStatusCombo, descriptionForTaskRunStatus } from "./TaskRunStatus";
export const allTaskRunStatuses = [
"PENDING",
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
"INTERRUPTED",
"SYSTEM_FAILURE",
] as TaskRunStatusType[];
export const allTaskRunStatuses = Object.values(TaskRunStatus) as TaskRunStatusType[];
export const TaskAttemptStatus = z.nativeEnum(TaskRunStatus);
export const TaskRunListSearchFilters = z.object({
@@ -151,10 +168,20 @@ export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersP
<SelectGroup>
<Select name="status" value={statuses?.at(0) ?? "ALL"} onValueChange={handleStatusChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder="Select status" className="ml-2 p-0" />
<SelectValue placeholder="Select status" className="ml-2 p-0">
{statuses?.at(0) ? (
<TaskRunStatusCombo
status={statuses[0]}
className="text-xs"
iconClassName="animate-none"
/>
) : (
"All statuses"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<SelectContent className="overflow-visible">
<SelectItem value={"ALL"} className="">
<Paragraph
variant="extra-small"
className="pl-0.5 transition group-hover:text-text-bright"
@@ -163,9 +190,24 @@ export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersP
</Paragraph>
</SelectItem>
{allTaskRunStatuses.map((status) => (
<SelectItem key={status} value={status}>
<TaskRunStatusCombo status={status} className="text-xs" />
</SelectItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="group flex w-full flex-col py-0">
<SelectItem key={status} value={status} className="">
<TaskRunStatusCombo
status={status}
className="text-xs"
iconClassName="animate-none"
/>
<TooltipContent side="right" sideOffset={9}>
<Paragraph variant="extra-small">
{descriptionForTaskRunStatus(status)}
</Paragraph>
</TooltipContent>
</SelectItem>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
))}
</SelectContent>
</Select>
@@ -8,6 +8,7 @@ import {
} from "@heroicons/react/20/solid";
import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database";
import { TaskRunAttemptStatus } from "@trigger.dev/database";
import { SnowflakeIcon } from "lucide-react";
import { Spinner } from "~/components/primitives/Spinner";
import { cn } from "~/utils/cn";
@@ -63,7 +64,7 @@ export function TaskRunAttemptStatusIcon({
case "EXECUTING":
return <Spinner className={cn(runAttemptStatusClassNameColor(status), className)} />;
case "PAUSED":
return <PauseCircleIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
return <SnowflakeIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
case "FAILED":
return <XCircleIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
case "CANCELED":
@@ -90,7 +91,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus
case "EXECUTING":
return "text-pending";
case "PAUSED":
return "text-amber-300";
return "text-sky-300";
case "FAILED":
return "text-error";
case "CANCELED":
@@ -117,7 +118,7 @@ export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null):
case "EXECUTING":
return "Executing";
case "PAUSED":
return "Paused";
return "Frozen";
case "FAILED":
return "Failed";
case "CANCELED":
@@ -10,19 +10,39 @@ import {
XCircleIcon,
} from "@heroicons/react/20/solid";
import { TaskRunStatus } from "@trigger.dev/database";
import { SnowflakeIcon } from "lucide-react";
import { Spinner } from "~/components/primitives/Spinner";
import { cn } from "~/utils/cn";
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
PENDING: "Task is waiting to be executed",
EXECUTING: "Task is currently being executed",
RETRYING_AFTER_FAILURE: "Task is being reattempted after a failure",
WAITING_TO_RESUME: "Task has been frozen and is waiting to be resumed",
COMPLETED_SUCCESSFULLY: "Task has been successfully completed",
CANCELED: "Task has been canceled",
COMPLETED_WITH_ERRORS: "Task has failed with errors",
INTERRUPTED: "Task has failed because it was interrupted",
SYSTEM_FAILURE: "Task has failed due to a system failure",
PAUSED: "Task has been paused by the user",
};
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
return taskRunStatusDescriptions[status];
}
export function TaskRunStatusCombo({
status,
className,
iconClassName,
}: {
status: TaskRunStatus;
className?: string;
iconClassName?: string;
}) {
return (
<span className={cn("flex items-center gap-1", className)}>
<TaskRunStatusIcon status={status} className="h-4 w-4" />
<TaskRunStatusIcon status={status} className={cn("h-4 w-4", iconClassName)} />
<TaskRunStatusLabel status={status} />
</span>
);
@@ -45,7 +65,7 @@ export function TaskRunStatusIcon({
case "EXECUTING":
return <Spinner className={cn(runStatusClassNameColor(status), className)} />;
case "WAITING_TO_RESUME":
return <ClockIcon className={cn(runStatusClassNameColor(status), className)} />;
return <SnowflakeIcon className={cn(runStatusClassNameColor(status), className)} />;
case "RETRYING_AFTER_FAILURE":
return <ArrowPathIcon className={cn(runStatusClassNameColor(status), className)} />;
case "PAUSED":
@@ -73,11 +93,10 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
case "PENDING":
return "text-charcoal-500";
case "EXECUTING":
case "RETRYING_AFTER_FAILURE":
return "text-pending";
case "WAITING_TO_RESUME":
return "text-charcoal-500";
case "RETRYING_AFTER_FAILURE":
return "text-charcoal-500";
return "text-sky-300";
case "PAUSED":
return "text-amber-300";
case "CANCELED":
@@ -100,13 +119,13 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
export function runStatusTitle(status: TaskRunStatus): string {
switch (status) {
case "PENDING":
return "Enqueued";
return "Queued";
case "EXECUTING":
return "Executing";
case "WAITING_TO_RESUME":
return "Waiting";
return "Frozen";
case "RETRYING_AFTER_FAILURE":
return "Retrying";
return "Reattempting";
case "PAUSED":
return "Paused";
case "CANCELED":
@@ -71,6 +71,7 @@ export class DeploymentPresenter {
},
status: true,
deployedAt: true,
createdAt: true,
promotions: {
select: {
label: true,
@@ -107,6 +108,7 @@ export class DeploymentPresenter {
shortCode: deployment.shortCode,
version: deployment.version,
status: deployment.status,
createdAt: deployment.createdAt,
deployedAt: deployment.deployedAt,
tasks: deployment.worker?.tasks,
label: deployment.promotions?.[0]?.label,
@@ -191,7 +191,7 @@ function PossibleIntegrationsList({
/>
</button>
}
defaultValue="integration"
defaultValue="feature"
/>
<Header2 className="mb-2 mt-6">Create an Integration</Header2>
@@ -1,53 +1,32 @@
import { CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid";
import { useLocation } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { TerminalIcon, TerminalSquareIcon } from "lucide-react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { UserAvatar } from "~/components/UserProfilePhoto";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { LinkButton } from "~/components/primitives/Buttons";
import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
import { Header2 } from "~/components/primitives/Headers";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import { ResizablePanel, ResizablePanelGroup } from "~/components/primitives/Resizable";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { DeploymentListPresenter } from "~/presenters/v3/DeploymentListPresenter.server";
import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import {
ProjectParamSchema,
docsPath,
runParam,
v3DeploymentParams,
v3DeploymentPath,
v3DeploymentsPath,
v3RunPath,
} from "~/utils/pathBuilder";
import { createSearchParams } from "~/utils/searchParams";
import { v3DeploymentParams, v3DeploymentsPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -111,6 +90,11 @@ export default function Page() {
<DeploymentStatus status={deployment.status} className="text-sm" />
</Property>
<Property label="Tasks">{deployment.tasks ? deployment.tasks.length : ""}</Property>
<Property label="Started at">
<Paragraph variant="small/bright">
<DateTimeAccurate date={deployment.createdAt} /> UTC
</Paragraph>
</Property>
<Property label="Deployed at">
<Paragraph variant="small/bright">
{deployment.deployedAt ? (
+1 -1
View File
@@ -13,8 +13,8 @@ export const feedbackTypeLabel = {
bug: "Bug report",
feature: "Feature request",
help: "Help me out",
integration: "Request an Integration",
enterprise: "Enterprise enquiry",
"developer preview": "Developer preview feedback",
};
export type FeedbackType = keyof typeof feedbackTypeLabel;
@@ -0,0 +1,11 @@
import { LoadingBarDivider } from "~/components/primitives/LoadingBarDivider";
const isLoading = true;
export default function Story() {
return (
<div className="grid h-full w-full max-w-3xl place-items-center px-20">
<LoadingBarDivider isLoading={isLoading} />
</div>
);
}
@@ -52,6 +52,10 @@ const stories: Story[] = [
name: "Inline code",
slug: "inline-code",
},
{
name: "Loading bar divider",
slug: "loading-bar-divider",
},
{
name: "NamedIcon",
slug: "named-icon",
+17 -2
View File
@@ -33,6 +33,7 @@ import { ResumeTaskService } from "./tasks/resumeTask.server";
import { ResumeTaskRunDependenciesService } from "~/v3/services/resumeTaskRunDependencies.server";
import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server";
import { ResumeTaskDependencyService } from "~/v3/services/resumeTaskDependency.server";
import { TimeoutDeploymentService } from "~/v3/services/timeoutDeployment.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -121,6 +122,11 @@ const workerCatalog = {
dependencyId: z.string(),
sourceTaskAttemptId: z.string(),
}),
"v3.timeoutDeployment": z.object({
deploymentId: z.string(),
fromStatus: z.string(),
errorMessage: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -295,8 +301,8 @@ function getWorkerQueue() {
graphileJob.id,
payload.orphanedEvents
? {
event: payload.orphanedEvents,
}
event: payload.orphanedEvents,
}
: undefined
);
break;
@@ -484,6 +490,15 @@ function getWorkerQueue() {
return await service.call(payload.dependencyId, payload.sourceTaskAttemptId);
},
},
"v3.timeoutDeployment": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new TimeoutDeploymentService();
return await service.call(payload.deploymentId, payload.fromStatus, payload.errorMessage);
},
},
},
});
}
@@ -21,6 +21,7 @@ import { CancelAttemptService } from "../services/cancelAttempt.server";
import { socketIo } from "../handleSocketIo.server";
import { singleton } from "~/utils/singleton";
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
import { findCurrentWorkerDeployment } from "../models/workerDeployment.server";
const tracer = trace.getTracer("sharedQueueConsumer");
@@ -179,7 +180,7 @@ export class SharedQueueConsumer {
this._taskFailures = 0;
this._taskSuccesses = 0;
this.#doWork().finally(() => {});
this.#doWork().finally(() => { });
}
async #doWork() {
@@ -316,26 +317,7 @@ export class SharedQueueConsumer {
return;
}
const deployment = await prisma.workerDeployment.findFirst({
where: {
environmentId: existingTaskRun.runtimeEnvironmentId,
projectId: existingTaskRun.projectId,
status: "DEPLOYED",
imageReference: {
not: null,
},
},
orderBy: {
updatedAt: "desc",
},
include: {
worker: {
include: {
tasks: true,
},
},
},
});
const deployment = await findCurrentWorkerDeployment(existingTaskRun.runtimeEnvironmentId);
if (!deployment || !deployment.worker) {
logger.error("No matching deployment found for task run", {
@@ -0,0 +1,29 @@
import type { Prettify } from "@trigger.dev/core";
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";
import { prisma } from "~/db.server";
export type CurrentWorkerDeployment = Prettify<NonNullable<Awaited<ReturnType<typeof findCurrentWorkerDeployment>>>>;
export async function findCurrentWorkerDeployment(environmentId: string) {
const promotion = await prisma.workerDeploymentPromotion.findUnique({
where: {
environmentId_label: {
environmentId,
label: CURRENT_DEPLOYMENT_LABEL,
}
},
include: {
deployment: {
include: {
worker: {
include: {
tasks: true,
},
},
}
}
}
});
return promotion?.deployment;
}
@@ -1,41 +1,34 @@
import type { CheckpointRestoreEvent, CheckpointRestoreEventType } from "@trigger.dev/database";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { BaseService } from "./baseService.server";
export class CreateCheckpointRestoreEventService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
export class CreateCheckpointRestoreEventService extends BaseService {
public async call(params: {
checkpointId: string;
type: CheckpointRestoreEventType;
}): Promise<CheckpointRestoreEvent | undefined> {
return await $transaction(this.#prismaClient, async (tx) => {
const checkpoint = await this.#prismaClient.checkpoint.findUniqueOrThrow({
where: {
id: params.checkpointId,
},
});
logger.debug(`Creating checkpoint/restore event`, params);
const checkpointEvent = await this.#prismaClient.checkpointRestoreEvent.create({
data: {
checkpointId: checkpoint.id,
runtimeEnvironmentId: checkpoint.runtimeEnvironmentId,
projectId: checkpoint.projectId,
attemptId: checkpoint.attemptId,
runId: checkpoint.runId,
type: params.type,
reason: checkpoint.reason,
metadata: checkpoint.metadata,
},
});
return checkpointEvent;
const checkpoint = await this._prisma.checkpoint.findUniqueOrThrow({
where: {
id: params.checkpointId,
},
});
logger.debug(`Creating checkpoint/restore event`, params);
const checkpointEvent = await this._prisma.checkpointRestoreEvent.create({
data: {
checkpointId: checkpoint.id,
runtimeEnvironmentId: checkpoint.runtimeEnvironmentId,
projectId: checkpoint.projectId,
attemptId: checkpoint.attemptId,
runId: checkpoint.runId,
type: params.type,
reason: checkpoint.reason,
metadata: checkpoint.metadata,
},
});
return checkpointEvent;
}
}
@@ -27,6 +27,10 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
return;
}
if (deployment.status !== "DEPLOYING") {
return;
}
const backgroundWorker = await this._prisma.backgroundWorker.create({
data: {
friendlyId: generateFriendlyId("worker"),
@@ -1,8 +1,10 @@
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { socketIo } from "../handleSocketIo.server";
import { BaseService } from "./baseService.server";
import { env } from "~/env.server";
import { DeploymentIndexFailed } from "./deploymentIndexFailed.server";
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
import { workerQueue } from "~/services/worker.server";
export class IndexDeploymentService extends BaseService {
public async call(id: string) {
@@ -43,7 +45,7 @@ export class IndexDeploymentService extends BaseService {
// just broadcast for now - there should only ever be one provider connected
try {
const responses = await socketIo.providerNamespace.timeout(10_000).emitWithAck("INDEX", {
const responses = await socketIo.providerNamespace.timeout(30_000).emitWithAck("INDEX", {
version: "v1",
shortCode: deployment.shortCode,
imageTag: deployment.imageReference,
@@ -52,15 +54,42 @@ export class IndexDeploymentService extends BaseService {
apiUrl: env.APP_ORIGIN,
});
const indexFailed = new DeploymentIndexFailed();
logger.debug("Index ACK received", { responses });
for (const response of responses) {
if (!response.success) {
await indexFailed.call(deployment.friendlyId, response.error);
if (responses.length === 0) {
// timeout the deployment if 50 seconds have passed and the deployment is still not indexed
await TimeoutDeploymentService.enqueue(
deployment.id,
"DEPLOYING",
"Could not index deployment in time",
new Date(Date.now() + 50_000)
);
} else {
const indexFailed = new DeploymentIndexFailed();
for (const response of responses) {
if (!response.success) {
await indexFailed.call(deployment.friendlyId, response.error);
}
}
}
} catch (error) {
logger.error("No index ACK received within timeout", { error });
const indexFailed = new DeploymentIndexFailed();
await indexFailed.call(
deployment.friendlyId,
error instanceof Error
? { message: error.message, name: error.name }
: { message: "Could not index deployment in time", name: "TimeoutError" }
);
}
}
static async enqueue(id: string) {
const runAt = new Date(Date.now() + 1000); // 1 second from now (give eventually-consistent DO time)
await workerQueue.enqueue("v3.indexDeployment", { id }, { runAt });
}
}
@@ -1,10 +1,11 @@
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { BaseService } from "./baseService.server";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { customAlphabet } from "nanoid";
import { createRemoteImageBuild } from "../remoteImageBuilder.server";
import { InitializeDeploymentRequestBody } from "@trigger.dev/core/v3";
import { customAlphabet } from "nanoid";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { createRemoteImageBuild } from "../remoteImageBuilder.server";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { BaseService } from "./baseService.server";
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8);
@@ -56,6 +57,13 @@ export class InitializeDeploymentService extends BaseService {
},
});
await TimeoutDeploymentService.enqueue(
deployment.id,
"BUILDING",
"Building timed out",
new Date(Date.now() + 180_000) // 3 minutes
);
const imageTag = `trigger/${environment.project.externalRef}:${deployment.version}.${environment.slug}`;
return { deployment, imageTag };
@@ -1,8 +1,8 @@
import { StartDeploymentIndexingRequestBody } from "@trigger.dev/core/v3";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { workerQueue } from "~/services/worker.server";
import { BaseService } from "./baseService.server";
import { registryProxy } from "../registryProxy.server";
import { BaseService } from "./baseService.server";
import { IndexDeploymentService } from "./indexDeployment.server";
export class StartDeploymentIndexing extends BaseService {
public async call(
@@ -22,7 +22,7 @@ export class StartDeploymentIndexing extends BaseService {
},
});
await workerQueue.enqueue("v3.indexDeployment", { id: deployment.id });
await IndexDeploymentService.enqueue(deployment.id);
return deployment;
}
@@ -0,0 +1,57 @@
import { logger } from "~/services/logger.server";
import { BaseService } from "./baseService.server";
import { workerQueue } from "~/services/worker.server";
export class TimeoutDeploymentService extends BaseService {
public async call(id: string, fromStatus: string, errorMessage: string) {
const deployment = await this._prisma.workerDeployment.findUnique({
where: {
id,
},
include: {
environment: true,
},
});
if (!deployment) {
logger.error(`No worker deployment with this ID: ${id}`);
return;
}
if (deployment.status !== fromStatus) {
return;
}
await this._prisma.workerDeployment.update({
where: {
id: deployment.id,
},
data: {
status: "TIMED_OUT",
failedAt: new Date(),
errorData: { message: errorMessage, name: "TimeoutError" },
},
});
}
static async enqueue(
deploymentId: string,
fromStatus: string,
errorMessage: string,
runAt: Date
) {
await workerQueue.enqueue(
"v3.timeoutDeployment",
{
deploymentId: deploymentId,
fromStatus,
errorMessage,
},
{
runAt,
jobKey: `timeoutDeployment:${deploymentId}`,
jobKeyMode: "replace",
}
);
}
}
+7 -1
View File
@@ -387,6 +387,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
throw new SkipLoggingError("Deployment was canceled");
}
case "TIMED_OUT": {
deploymentSpinner.stop(`Deployment timed out. ${deploymentLink}`);
throw new SkipLoggingError("Deployment timed out");
}
}
}
@@ -479,7 +484,8 @@ async function waitForDeploymentToFinish(
if (
deployment.data.status === "DEPLOYED" ||
deployment.data.status === "FAILED" ||
deployment.data.status === "CANCELED"
deployment.data.status === "CANCELED" ||
deployment.data.status === "TIMED_OUT"
) {
span.setAttributes({
"deployment.status": deployment.data.status,
+3 -1
View File
@@ -48,7 +48,9 @@ export async function whoAmI(
options?: WhoamiCommandOptions,
embedded: boolean = false
): Promise<WhoAmIResult> {
intro(`Displaying your account details [${options?.profile ?? "default"}]`);
if (!embedded) {
intro(`Displaying your account details [${options?.profile ?? "default"}]`);
}
const loadingSpinner = spinner();
loadingSpinner.start("Checking your account details");
+2 -2
View File
@@ -155,7 +155,7 @@ export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymen
export const GetDeploymentResponseBody = z.object({
id: z.string(),
status: z.enum(["PENDING", "BUILDING", "DEPLOYING", "DEPLOYED", "FAILED", "CANCELED"]),
status: z.enum(["PENDING", "BUILDING", "DEPLOYING", "DEPLOYED", "FAILED", "CANCELED", "TIMED_OUT"]),
contentHash: z.string(),
shortCode: z.string(),
version: z.string(),
@@ -164,7 +164,7 @@ export const GetDeploymentResponseBody = z.object({
.object({
name: z.string(),
message: z.string(),
stack: z.string(),
stack: z.string().optional(),
})
.optional()
.nullable(),
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "WorkerDeploymentStatus" ADD VALUE 'TIMED_OUT';
+2
View File
@@ -2065,6 +2065,8 @@ enum WorkerDeploymentStatus {
DEPLOYED
FAILED
CANCELED
/// This is the status when the image is built and indexing does not finish in time
TIMED_OUT
}
model WorkerDeploymentPromotion {