Merge branch 'main' into v3/infra-updates
This commit is contained in:
@@ -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>
|
||||
|
||||
+7
-23
@@ -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 ? (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "WorkerDeploymentStatus" ADD VALUE 'TIMED_OUT';
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user