Features: Disabling and deleting jobs (#381)
* Don’t show the Ready To Run Job prompt if you have an Integration that needs attention * Don’t highlight the row red or green * Improvements to the contrast of the app sections and dividers * Removed the Jobs page title as it’s duped info * using the new border variable * Sticky last table cell * Improved the sticky last table cell * Make any last cell in a table sticky by adding isSticky to it * Added a dropdown menu to the menu table cell * table rows can be marked as disabled by adding disabled * Using the jobTestPath function for the test path * tidy up imports * Removed un-used props * Removed the green badge variant * Added a new status badge to the Runs table * Clicking the gradient clicks the row * Delete Job triggers a modal popup * Added a large danger button type * Added some modal styling and started adding data * Added more styling and data to the delete job modal * A table can now be given a full width prop * Large danger button added to Storybook * Danger button disabled state looks disabled now * New active badge component to display in the table and logic for showing the env data * Style updates to the dialog component * active and job status badges can now have a small size * Added a new named icon * The Job page shows the Job status in the PageInfoRow * Small badge style update * Runs table has a sticky right cell * Created a JobStatusTable component * Added some placeholder help panel content for disabling a Job * WIP creating a Settings page * Added a delete button that triggers the delete modal – just need data hooking up * Implemented deleting jobs from the dashboard --------- Co-authored-by: Eric Allam <eallam@icloud.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variant = {
|
||||
small:
|
||||
"py-[0.25rem] px-1.5 text-xxs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-[0.125rem]",
|
||||
normal:
|
||||
"py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm",
|
||||
};
|
||||
|
||||
type ActiveBadgeProps = {
|
||||
active: boolean;
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
};
|
||||
|
||||
export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveBadgeProps) {
|
||||
switch (active) {
|
||||
case true:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-green-500", className)}>
|
||||
Active
|
||||
</span>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-dimmed", className)}>
|
||||
Disabled
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function MissingIntegrationBadge({
|
||||
className,
|
||||
badgeSize = "normal",
|
||||
}: {
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-rose-600 text-white", className)}>
|
||||
Missing Integration
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewBadge({
|
||||
className,
|
||||
badgeSize = "normal",
|
||||
}: {
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-green-600 text-background", className)}>New!</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { EnvironmentLabel } from "./environments/EnvironmentLabel";
|
||||
import { DateTime } from "./primitives/DateTime";
|
||||
import { ActiveBadge } from "./ActiveBadge";
|
||||
|
||||
export type JobEnvironment = {
|
||||
type: RuntimeEnvironmentType;
|
||||
lastRun?: Date;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type JobStatusTableProps = {
|
||||
environments: JobEnvironment[];
|
||||
};
|
||||
|
||||
export function JobStatusTable({ environments }: JobStatusTableProps) {
|
||||
return (
|
||||
<Table fullWidth>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Last Run</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Version</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Status</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{environments.map((environment, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
|
||||
</TableCell>
|
||||
<TableCell alignment="right">{environment.version}</TableCell>
|
||||
<TableCell alignment="right">
|
||||
<ActiveBadge active={environment.enabled} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
|
||||
"inline-flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
|
||||
environmentColorClassName(environment),
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -414,6 +414,60 @@ export function HowToUseThisIntegration({ integration, help, integrationClient }
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToDisableAJob({
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
To disable a job, you need to set the <InlineCode>enabled</InlineCode> property to{" "}
|
||||
<InlineCode>false</InlineCode>.
|
||||
</Paragraph>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title={
|
||||
<>
|
||||
Set <InlineCode>enabled</InlineCode> to <InlineCode>false</InlineCode>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`client.defineJob({
|
||||
id: "${id}",
|
||||
name: "${name}",
|
||||
version: "${version}",
|
||||
enabled: false,
|
||||
// ...rest of your Job definition
|
||||
});`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="2"
|
||||
title={
|
||||
<>
|
||||
Run the <InlineCode>@trigger.dev/cli dev</InlineCode> command
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
If you aren't already running the <InlineCode>dev</InlineCode> command, run it now.
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseApiKeysAndEndpoints() {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { JobStatusTable } from "../JobsStatusTable";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
type JobEnvironment = {
|
||||
type: RuntimeEnvironmentType;
|
||||
lastRun?: Date;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type DeleteJobDialogContentProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
environments: JobEnvironment[];
|
||||
redirectTo?: string;
|
||||
};
|
||||
|
||||
export function DeleteJobDialogContent({
|
||||
title,
|
||||
slug,
|
||||
environments,
|
||||
id,
|
||||
redirectTo,
|
||||
}: DeleteJobDialogContentProps) {
|
||||
const canDelete = environments.every((environment) => !environment.enabled);
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const isLoading =
|
||||
fetcher.state === "submitting" ||
|
||||
(fetcher.state === "loading" && fetcher.formMethod === "DELETE");
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-y-6">
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<Header1>{title}</Header1>
|
||||
<Paragraph variant="small">ID: {slug}</Paragraph>
|
||||
</div>
|
||||
<JobStatusTable environments={environments} />
|
||||
|
||||
<Header2
|
||||
className={cn(
|
||||
canDelete ? "border-rose-500 bg-rose-500/10" : "border-amber-500 bg-amber-500/10",
|
||||
"rounded border px-3.5 py-2 text-center text-bright"
|
||||
)}
|
||||
>
|
||||
{canDelete
|
||||
? "Are you sure you want to delete this Job?"
|
||||
: "You can't delete this Job until all env are disabled"}
|
||||
</Header2>
|
||||
<Paragraph variant="small" className="px-6 text-center">
|
||||
{canDelete ? (
|
||||
<>
|
||||
This will permanently delete the Job <span className="strong text-bright">{title}</span>
|
||||
. This includes the deletion of all Run history. This cannot be undone.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This Job is still active in an environment. You need to disable it in your Job code
|
||||
first before it can be deleted.{" "}
|
||||
<TextLink to="https://trigger.dev/docs/documentation/guides/jobs/managing#disabling-jobs">
|
||||
Learn how to disable a Job
|
||||
</TextLink>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</Paragraph>
|
||||
{canDelete ? (
|
||||
<fetcher.Form
|
||||
method="delete"
|
||||
action={`/resources/jobs/${id}${redirectTo ? `?redirectTo=${redirectTo}` : ""}`}
|
||||
>
|
||||
<Button variant="danger/large" fullWidth>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<NamedIcon
|
||||
name="trash-can"
|
||||
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
|
||||
/>
|
||||
Delete this job
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
) : (
|
||||
<Button variant="danger/large" fullWidth disabled>
|
||||
<>
|
||||
<NamedIcon
|
||||
name="trash-can"
|
||||
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
|
||||
/>
|
||||
Delete this job
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge";
|
||||
|
||||
type JobStatusBadgeProps = {
|
||||
enabled: boolean;
|
||||
hasIntegrationsRequiringAction: boolean;
|
||||
hasRuns: boolean;
|
||||
badgeSize?: "small" | "normal";
|
||||
};
|
||||
|
||||
export function JobStatusBadge({
|
||||
enabled,
|
||||
hasIntegrationsRequiringAction,
|
||||
hasRuns,
|
||||
badgeSize = "normal",
|
||||
}: JobStatusBadgeProps) {
|
||||
if (!enabled) {
|
||||
return <ActiveBadge active={false} badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
if (hasIntegrationsRequiringAction) {
|
||||
return <MissingIntegrationBadge badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
if (!hasRuns) {
|
||||
return <NewBadge badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
return <ActiveBadge active={true} badgeSize={badgeSize} />;
|
||||
}
|
||||
@@ -1,26 +1,29 @@
|
||||
import { jobPath } from "~/utils/pathBuilder";
|
||||
import { ProjectJob } from "~/hooks/useJobs";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { jobPath, jobTestPath } from "~/utils/pathBuilder";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { PopoverMenuItem } from "../primitives/Popover";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../primitives/Table";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { runStatusTitle } from "../runs/RunStatuses";
|
||||
import { ProjectJob } from "~/hooks/useJobs";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Badge } from "../primitives/Badge";
|
||||
import { DeleteJobDialogContent } from "./DeleteJobModalContent";
|
||||
import { JobStatusBadge } from "./JobStatusBadge";
|
||||
|
||||
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
|
||||
const organization = useOrganization();
|
||||
@@ -35,6 +38,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
|
||||
<TableHeaderCell>Integrations</TableHeaderCell>
|
||||
<TableHeaderCell>Properties</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -43,13 +47,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
|
||||
jobs.map((job) => {
|
||||
const path = jobPath(organization, project, job);
|
||||
return (
|
||||
<TableRow
|
||||
key={job.id}
|
||||
className={cn(
|
||||
(job.hasIntegrationsRequiringAction && "bg-rose-500/20") ||
|
||||
(job.lastRun === undefined && "bg-green-500/20")
|
||||
)}
|
||||
>
|
||||
<TableRow key={job.id} className="group">
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-2">
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
@@ -145,13 +143,39 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
|
||||
<LabelValueStack label={"Never run"} value={"–"} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path}>
|
||||
{job.lastRun === undefined && (
|
||||
<Badge className="mr-4" variant="green">
|
||||
New Job!
|
||||
</Badge>
|
||||
)}
|
||||
</TableCellChevron>
|
||||
<TableCell to={path}>
|
||||
<JobStatusBadge
|
||||
enabled={job.status === "ACTIVE"}
|
||||
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
|
||||
hasRuns={job.lastRun !== undefined}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCellMenu isSticky>
|
||||
<PopoverMenuItem to={path} title="View Job" icon="eye" />
|
||||
<PopoverMenuItem
|
||||
to={jobTestPath(organization, project, job)}
|
||||
title="Test Job"
|
||||
icon="beaker"
|
||||
/>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="menu-item" LeadingIcon="trash-can">
|
||||
Delete Job
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DeleteJobDialogContent
|
||||
id={job.id}
|
||||
title={job.title}
|
||||
slug={job.slug}
|
||||
environments={job.environments}
|
||||
/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableCellMenu>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ import { docsRoot } from "~/utils/pathBuilder";
|
||||
|
||||
export function NavBar() {
|
||||
return (
|
||||
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-divide py-1 pl-1 pr-2.5">
|
||||
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-uiBorder py-1 pl-1 pr-2.5">
|
||||
<div className="flex gap-0.5">
|
||||
<Link to="/" className="p-2">
|
||||
<LogoIcon className="h-5 w-5" />
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ProjectSideMenu() {
|
||||
variants={menuVariants}
|
||||
initial={isCollapsed ? "collapsed" : "expanded"}
|
||||
className={cn(
|
||||
"flex h-full flex-col justify-between overflow-hidden border-r border-slate-850 p-1 transition duration-300 ease-in-out"
|
||||
"flex h-full flex-col justify-between overflow-hidden border-r border-uiBorder p-1 transition duration-300 ease-in-out"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -6,8 +6,6 @@ const variants = {
|
||||
"grid place-items-center rounded-full px-2 h-5 tracking-wider text-xxs bg-slate-700 text-bright uppercase whitespace-nowrap",
|
||||
outline:
|
||||
"grid place-items-center rounded-sm px-1 h-5 tracking-wider text-xxs border border-dimmed text-dimmed uppercase whitespace-nowrap",
|
||||
green:
|
||||
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider outline-offset-1 outline outline-1 outline-green-600 text-xxs bg-green-500 text-slate-900 uppercase whitespace-nowrap",
|
||||
};
|
||||
|
||||
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
|
||||
@@ -87,6 +87,14 @@ const variant = {
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
"danger/large": {
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
|
||||
button:
|
||||
"h-10 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
|
||||
icon: "h-5",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"menu-item": {
|
||||
textColor: "text-bright",
|
||||
button:
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
@@ -25,7 +26,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100",
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -42,15 +43,29 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in sm:max-w-lg sm:rounded-lg sm:zoom-in-90",
|
||||
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-midnight-800 p-6 pt-11 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="-ml-6 w-[calc(100%_+_3rem)]" />
|
||||
{children}
|
||||
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
|
||||
<div className="flex gap-x-2">
|
||||
<ShortcutKey
|
||||
shortcut={{
|
||||
windows: {
|
||||
key: "esc",
|
||||
},
|
||||
mac: {
|
||||
key: "esc",
|
||||
},
|
||||
}}
|
||||
variant={"small"}
|
||||
/>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
<span className="sr-only">Close</span>
|
||||
</div>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function FormTitle({
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 flex flex-col gap-3 pb-4",
|
||||
divide ? "border-b border-slate-800" : "",
|
||||
divide ? "border-b border-uiBorder" : "",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Cog8ToothIcon,
|
||||
CreditCardIcon,
|
||||
EnvelopeIcon,
|
||||
EyeIcon,
|
||||
FingerPrintIcon,
|
||||
FlagIcon,
|
||||
FolderIcon,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
QrCodeIcon,
|
||||
SquaresPlusIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
UserCircleIcon,
|
||||
UserGroupIcon,
|
||||
UserIcon,
|
||||
@@ -50,7 +52,7 @@ import {
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import { HourglassIcon } from "lucide-react";
|
||||
import { ActivityIcon, HourglassIcon } from "lucide-react";
|
||||
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
|
||||
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
|
||||
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
|
||||
@@ -100,6 +102,7 @@ const icons = {
|
||||
folder: (className: string) => <FolderIcon className={cn("text-indigo-600", className)} />,
|
||||
envelope: (className: string) => <EnvelopeIcon className={cn("text-cyan-500", className)} />,
|
||||
environment: (className: string) => <KeyIcon className={cn("text-yellow-500", className)} />,
|
||||
eye: (className: string) => <EyeIcon className={cn("text-blue-500", className)} />,
|
||||
globe: (className: string) => <GlobeAltIcon className={cn("text-fuchsia-600", className)} />,
|
||||
"hand-raised": (className: string) => (
|
||||
<HandRaisedIcon className={cn("text-amber-400", className)} />
|
||||
@@ -129,6 +132,7 @@ const icons = {
|
||||
<PlusSmallIcon className={cn("text-green-600", className)} />
|
||||
),
|
||||
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
|
||||
pulse: (className: string) => <ActivityIcon className={cn("text-green-600", className)} />,
|
||||
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
|
||||
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
|
||||
sapling: (className: string) => <SaplingIcon className={cn("text-green-500", className)} />,
|
||||
@@ -139,6 +143,7 @@ const icons = {
|
||||
star: (className: string) => <StarIcon className={cn("text-yellow-500", className)} />,
|
||||
stop: (className: string) => <StopIcon className={cn("text-rose-500", className)} />,
|
||||
team: (className: string) => <UserGroupIcon className={cn("text-blue-500", className)} />,
|
||||
"trash-can": (className: string) => <TrashIcon className={cn("text-rose-500", className)} />,
|
||||
tree: (className: string) => <OneTreeIcon className={cn("text-green-500", className)} />,
|
||||
trees: (className: string) => <TwoTreesIcon className={cn("text-green-500", className)} />,
|
||||
trigger: (className: string) => <BoltIcon className={cn("text-orange-500", className)} />,
|
||||
|
||||
@@ -13,7 +13,7 @@ type WithChildren = {
|
||||
|
||||
export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) {
|
||||
return (
|
||||
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-slate-800 pb-4")}>
|
||||
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-uiBorder pb-4")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,8 @@ import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid";
|
||||
import { LinkButton } from "./Buttons";
|
||||
import { IconNames } from "./NamedIcon";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
@@ -91,6 +90,24 @@ function PopoverArrowTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverVerticalEllipseTrigger({
|
||||
isOpen,
|
||||
className,
|
||||
...props
|
||||
}: { isOpen?: boolean } & React.ComponentPropsWithoutRef<typeof PopoverTrigger>) {
|
||||
return (
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex items-center justify-end gap-1 rounded px-1.5 py-1.5 text-dimmed transition hover:bg-slate-750 hover:text-bright",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<EllipsisVerticalIcon className={cn("h-5 w-5 transition group-hover:text-bright")} />
|
||||
</PopoverTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
@@ -98,4 +115,5 @@ export {
|
||||
PopoverSectionHeader,
|
||||
PopoverArrowTrigger,
|
||||
PopoverMenuItem,
|
||||
PopoverVerticalEllipseTrigger,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ const SheetOverlay = React.forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-y border-slate-800",
|
||||
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-uiBorder",
|
||||
{
|
||||
variants: {
|
||||
position: {
|
||||
@@ -154,7 +154,7 @@ const SheetContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 p-2">
|
||||
<div className="flex items-center gap-2 border-b border-uiBorder p-2">
|
||||
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
|
||||
<NamedIcon name="close" className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
@@ -181,7 +181,7 @@ export const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivE
|
||||
export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-4 flex shrink-0 items-center gap-4 border-b border-slate-800 py-3.5",
|
||||
"mx-4 flex shrink-0 items-center gap-4 border-b border-uiBorder py-3.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -194,7 +194,7 @@ export const SheetFooter = ({
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("shrink-0", className)} {...props}>
|
||||
<div className="mx-4 border-t border-slate-800 py-3">{children}</div>
|
||||
<div className="mx-4 border-t border-uiBorder py-3">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import { ReactNode, forwardRef, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Badge } from "./Badge";
|
||||
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
|
||||
|
||||
type TableProps = {
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export const Table = forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, containerClassName, children }, ref) => {
|
||||
({ className, containerClassName, children, fullWidth }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto whitespace-nowrap rounded-md border border-slate-900 scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
|
||||
containerClassName
|
||||
"overflow-x-auto whitespace-nowrap rounded-md border border-uiBorder scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
|
||||
containerClassName,
|
||||
fullWidth && "w-full"
|
||||
)}
|
||||
>
|
||||
<table ref={ref} className={cn("w-full divide-y bg-midnight-850", className)}>
|
||||
<table ref={ref} className={cn("w-full divide-y", className)}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@@ -37,7 +39,7 @@ export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("rounded-t-md", "relative divide-y divide-slate-850", className)}
|
||||
className={cn("rounded-t-md", "relative divide-y divide-uiBorder bg-slate-850", className)}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
@@ -53,7 +55,7 @@ type TableBodyProps = {
|
||||
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
||||
({ className, children }, ref) => {
|
||||
return (
|
||||
<tbody ref={ref} className={cn("relative divide-y divide-slate-850", className)}>
|
||||
<tbody ref={ref} className={cn("relative divide-y divide-uiBorder", className)}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
@@ -63,12 +65,13 @@ export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
||||
type TableRowProps = {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ className, children }, ref) => {
|
||||
({ className, disabled, children }, ref) => {
|
||||
return (
|
||||
<tr ref={ref} className={cn("group w-full", className)}>
|
||||
<tr ref={ref} className={cn(disabled && "opacity-50", "group w-full", className)}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
@@ -103,7 +106,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
|
||||
ref={ref}
|
||||
scope="col"
|
||||
className={cn(
|
||||
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
|
||||
"px-4 py-3 align-middle text-xs font-normal uppercase tracking-wider text-dimmed",
|
||||
alignmentClassName,
|
||||
className
|
||||
)}
|
||||
@@ -118,10 +121,11 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
|
||||
type TableCellProps = TableCellBasicProps & {
|
||||
to?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
hasAction?: boolean;
|
||||
};
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ className, alignment = "left", children, colSpan, to, onClick }, ref) => {
|
||||
({ className, alignment = "left", children, colSpan, to, onClick, hasAction = false }, ref) => {
|
||||
let alignmentClassName = "text-left";
|
||||
switch (alignment) {
|
||||
case "center":
|
||||
@@ -133,7 +137,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
}
|
||||
|
||||
const flexClasses = cn(
|
||||
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
|
||||
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-dimmed",
|
||||
alignment === "left"
|
||||
? "justify-start text-left"
|
||||
: alignment === "center"
|
||||
@@ -145,8 +149,10 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs text-slate-400 transition group-hover:bg-slate-850/50",
|
||||
to || onClick ? "cursor-pointer" : "px-4 py-3 align-middle",
|
||||
"text-xs text-slate-400",
|
||||
to || onClick || hasAction
|
||||
? "cursor-pointer group-hover:bg-slate-900"
|
||||
: "px-4 py-3 align-middle",
|
||||
!to && !onClick && alignmentClassName,
|
||||
className
|
||||
)}
|
||||
@@ -168,23 +174,64 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
}
|
||||
);
|
||||
|
||||
const stickyStyles =
|
||||
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
|
||||
|
||||
export const TableCellChevron = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
className?: string;
|
||||
to?: string;
|
||||
children?: ReactNode;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
>(({ className, to, children, onClick }, ref) => {
|
||||
>(({ className, to, children, isSticky, onClick }, ref) => {
|
||||
return (
|
||||
<TableCell className={className} to={to} onClick={onClick} ref={ref} alignment="right">
|
||||
<TableCell
|
||||
className={cn(isSticky && stickyStyles, className)}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
|
||||
</TableCell>
|
||||
);
|
||||
});
|
||||
|
||||
export const TableCellMenu = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
>(({ className, children, isSticky, onClick }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<TableCell
|
||||
className={cn(isSticky && stickyStyles, className)}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
hasAction={true}
|
||||
>
|
||||
<Popover onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverVerticalEllipseTrigger isOpen={isOpen} />
|
||||
<PopoverContent
|
||||
className="w-fit max-w-[10rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{children}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
);
|
||||
});
|
||||
|
||||
type TableBlankRowProps = {
|
||||
className?: string;
|
||||
colSpan: number;
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TabsProps = {
|
||||
|
||||
export function Tabs({ tabs, className }: TabsProps) {
|
||||
return (
|
||||
<div className={cn(`flex flex-row gap-x-6 border-b border-slate-800`, className)}>
|
||||
<div className={cn(`flex flex-row gap-x-6 border-b border-uiBorder`, className)}>
|
||||
{tabs.map((tab, index) => (
|
||||
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
|
||||
{({ isActive, isPending }) => (
|
||||
|
||||
@@ -109,7 +109,7 @@ export function RunsTable({
|
||||
<TableCell to={path}>
|
||||
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
<TableCellChevron to={path} isSticky />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@ function BadgesExample() {
|
||||
<div className="flex flex-col items-start gap-y-8 p-8">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="green">Green</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,13 +253,11 @@ function ButtonList({ primary }: { primary: string }) {
|
||||
<Header1 className="mb-2 mt-8">Large buttons</Header1>
|
||||
<div className="grid grid-cols-1 gap-8 border-b border-slate-700 pb-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Button variant="primary/large" fullWidth>
|
||||
<NamedIcon name={"github"} className={"mr-1.5 h-4 w-4"} />
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<Button variant="secondary/large" fullWidth>
|
||||
<NamedIcon
|
||||
name={"envelope"}
|
||||
@@ -267,6 +265,13 @@ function ButtonList({ primary }: { primary: string }) {
|
||||
/>
|
||||
Continue with Email
|
||||
</Button>
|
||||
<Button variant="danger/large" fullWidth>
|
||||
<NamedIcon
|
||||
name={"trash-can"}
|
||||
className={"mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"}
|
||||
/>
|
||||
This is a delete button
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,10 @@ export type ProjectJob = UseDataFunctionReturn<typeof loader>["projectJobs"][num
|
||||
|
||||
export const jobsMatchId =
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
|
||||
|
||||
// This is only used in the JobsMenu component, which is the breadcrumb job list dropdown.
|
||||
// This dropdown is only shown once you have selected a job, so we can assume that
|
||||
// the route above has loaded and we can use the data from it.
|
||||
export function useOptionalJobs(matches?: RouteMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: jobsMatchId,
|
||||
|
||||
@@ -102,6 +102,25 @@ export async function jsonWithSuccessMessage(
|
||||
});
|
||||
}
|
||||
|
||||
export async function jsonWithErrorMessage(
|
||||
data: any,
|
||||
request: Request,
|
||||
message: string,
|
||||
options?: ToastMessageOptions
|
||||
) {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
|
||||
setErrorMessage(session, message, options);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session, {
|
||||
expires: new Date(Date.now() + ONE_YEAR),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function redirectWithSuccessMessage(
|
||||
path: string,
|
||||
request: Request,
|
||||
|
||||
@@ -49,6 +49,7 @@ export function getOrganizations({ userId }: { userId: User["id"] }) {
|
||||
jobs: {
|
||||
where: {
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@ export class IntegrationClientPresenter {
|
||||
slug: projectSlug,
|
||||
},
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -71,6 +71,7 @@ export class IntegrationsPresenter {
|
||||
slug: projectSlug,
|
||||
},
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ export class JobListPresenter {
|
||||
version: true,
|
||||
eventSpecification: true,
|
||||
properties: true,
|
||||
status: true,
|
||||
runs: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
@@ -92,6 +93,7 @@ export class JobListPresenter {
|
||||
},
|
||||
where: {
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
organization: orgWhere,
|
||||
project: {
|
||||
slug: projectSlug,
|
||||
@@ -162,11 +164,19 @@ export class JobListPresenter {
|
||||
properties = [...properties, ...versionProperties];
|
||||
}
|
||||
|
||||
const environments = job.aliases.map((alias) => ({
|
||||
type: alias.environment.type,
|
||||
enabled: alias.version.status === "ACTIVE",
|
||||
lastRun: alias.version.runs.at(0)?.createdAt,
|
||||
version: alias.version.version,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: job.id,
|
||||
slug: job.slug,
|
||||
title: job.title,
|
||||
version: alias.version.version,
|
||||
status: alias.version.status,
|
||||
dynamic: job.dynamicTriggers.length > 0,
|
||||
event: {
|
||||
title: eventSpecification.title,
|
||||
@@ -179,6 +189,7 @@ export class JobListPresenter {
|
||||
),
|
||||
lastRun,
|
||||
properties,
|
||||
environments,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -80,6 +80,7 @@ export class ProjectPresenter {
|
||||
},
|
||||
where: {
|
||||
internal: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: [{ title: "asc" }],
|
||||
},
|
||||
|
||||
+6
-7
@@ -73,8 +73,6 @@ export default function Page() {
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
|
||||
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
@@ -104,7 +102,6 @@ export default function Page() {
|
||||
</Callout>
|
||||
)}
|
||||
<div className="mb-2 flex flex-col">
|
||||
<Header2 spacing>Jobs</Header2>
|
||||
<div className="flex w-full">
|
||||
<Input
|
||||
placeholder="Search Jobs"
|
||||
@@ -122,9 +119,11 @@ export default function Page() {
|
||||
noResultsText={`No Jobs match ${filterText}. Try a different search
|
||||
query.`}
|
||||
/>
|
||||
{jobs.length === 1 && jobs.every((r) => r.lastRun === undefined) && (
|
||||
<RunYourJobPrompt />
|
||||
)}
|
||||
{jobs.length === 1 &&
|
||||
jobs.every((r) => r.lastRun === undefined) &&
|
||||
jobs.every((i) => i.hasIntegrationsRequiringAction === false) && (
|
||||
<RunYourJobPrompt />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<HowToSetupYourProject />
|
||||
@@ -194,7 +193,7 @@ function ExampleJobs() {
|
||||
<a
|
||||
href={example.codeLink}
|
||||
key={example.title}
|
||||
className="flex w-full items-center rounded border-b border-slate-800 py-2 transition hover:border-transparent hover:bg-slate-800"
|
||||
className="flex w-full items-center rounded border-b border-uiBorder py-2 transition hover:border-transparent hover:bg-slate-800"
|
||||
>
|
||||
{example.icon}
|
||||
<Paragraph variant="small">
|
||||
|
||||
+3
-3
@@ -355,7 +355,7 @@ function ConnectedIntegrationsList({
|
||||
<TableCell to={path}>
|
||||
<DateTime date={client.createdAt} includeSeconds={false} />
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
<TableCellChevron to={path} isSticky />
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
@@ -454,7 +454,7 @@ function IntegrationsWithMissingFields({
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
button={
|
||||
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
|
||||
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
|
||||
}
|
||||
callbackUrl={callbackUrl}
|
||||
existingIntegration={client}
|
||||
@@ -482,7 +482,7 @@ function AddIntegrationConnection({
|
||||
icon?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-850">
|
||||
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-900">
|
||||
<NamedIconInBox
|
||||
name={icon ?? identifier}
|
||||
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
|
||||
|
||||
+61
-6
@@ -1,11 +1,66 @@
|
||||
import { ComingSoon } from "~/components/ComingSoon";
|
||||
import { JobStatusTable } from "~/components/JobsStatusTable";
|
||||
import { HowToDisableAJob } from "~/components/helpContent/HelpContentText";
|
||||
import { DeleteJobDialogContent } from "~/components/jobs/DeleteJobModalContent";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { projectJobsPath, projectPath } from "~/utils/pathBuilder";
|
||||
|
||||
export default function Page() {
|
||||
const job = useJob();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Job settings"
|
||||
description="Disable a Job, archive it and more."
|
||||
icon="settings"
|
||||
/>
|
||||
<Help defaultOpen>
|
||||
{(open) => (
|
||||
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header2 className="mb-2 flex items-center gap-1">Environments</Header2>
|
||||
<HelpTrigger title="How do disable a Job?" />
|
||||
</div>
|
||||
<JobStatusTable environments={job.environments} />
|
||||
<div className="mt-4 flex w-full items-center justify-end gap-x-3">
|
||||
<Paragraph variant="small">
|
||||
Disable this Job in all environments before deleting
|
||||
</Paragraph>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="danger/small"
|
||||
leadingIconClassName="text-bright"
|
||||
LeadingIcon="trash-can"
|
||||
>
|
||||
Delete Job
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DeleteJobDialogContent
|
||||
title={job.title}
|
||||
slug={job.slug}
|
||||
environments={job.environments}
|
||||
id={job.id}
|
||||
redirectTo={projectJobsPath(organization, project)}
|
||||
/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<HelpContent title="How to disable a Job">
|
||||
<HowToDisableAJob id={job.slug} version={job.version} name={job.title} />
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
);
|
||||
}
|
||||
|
||||
+13
@@ -2,6 +2,7 @@ import { Outlet, useLocation } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { JobStatusBadge } from "~/components/jobs/JobStatusBadge";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { JobsMenu } from "~/components/navigation/JobsMenu";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
@@ -134,6 +135,18 @@ export default function Job() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<PageInfoProperty
|
||||
icon="pulse"
|
||||
label={"STATUS"}
|
||||
value={
|
||||
<JobStatusBadge
|
||||
enabled={job.status === "ACTIVE"}
|
||||
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
|
||||
hasRuns={job.lastRun !== undefined}
|
||||
badgeSize="small"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageInfoGroup>
|
||||
<PageInfoGroup alignment="right">
|
||||
<Paragraph variant="extra-small" className="text-slate-600">
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function Page() {
|
||||
<OrgAdminHeader />
|
||||
<PageBody>
|
||||
<Header2>Members</Header2>
|
||||
<ul className="flex w-full max-w-md flex-col divide-y divide-slate-800 border-b border-slate-800">
|
||||
<ul className="flex w-full max-w-md flex-col divide-y divide-uiBorder border-b border-uiBorder">
|
||||
{members.map((member) => (
|
||||
<li key={member.user.id} className="flex items-center gap-x-4 py-4">
|
||||
<UserAvatar
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ActionFunction } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
jsonWithErrorMessage,
|
||||
jsonWithSuccessMessage,
|
||||
redirectWithSuccessMessage,
|
||||
} from "~/models/message.server";
|
||||
import { DeleteJobService } from "~/services/jobs/deleteJob.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const ParamSchema = z.object({
|
||||
jobId: z.string(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const { jobId } = ParamSchema.parse(params);
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
// Find the job
|
||||
const job = await prisma.job.findFirst({
|
||||
where: {
|
||||
id: jobId,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!job) {
|
||||
return jsonWithErrorMessage({ ok: false }, request, `Job could not be scheduled for deletion.`);
|
||||
}
|
||||
try {
|
||||
const deleteJobService = new DeleteJobService();
|
||||
|
||||
await deleteJobService.call(job);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const redirectTo = url.searchParams.get("redirectTo");
|
||||
|
||||
logger.debug("Job scheduled for deletion", {
|
||||
url,
|
||||
redirectTo,
|
||||
job,
|
||||
});
|
||||
|
||||
if (typeof redirectTo === "string" && redirectTo.length > 0) {
|
||||
return redirectWithSuccessMessage(
|
||||
redirectTo,
|
||||
request,
|
||||
`Job ${job.slug} has been scheduled for deletion.`
|
||||
);
|
||||
}
|
||||
|
||||
return jsonWithSuccessMessage(
|
||||
{ ok: true },
|
||||
request,
|
||||
`Job ${job.slug} has been scheduled for deletion.`
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
return jsonWithErrorMessage(
|
||||
{ ok: false },
|
||||
request,
|
||||
`Job could not be scheduled for deletion: ${message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -65,6 +65,7 @@ export class IndexEndpointService {
|
||||
const existingJobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
projectId: endpoint.projectId,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
aliases: {
|
||||
@@ -99,9 +100,11 @@ export class IndexEndpointService {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this.#registerJobService.call(endpoint, job);
|
||||
const registeredVersion = await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
indexStats.jobs++;
|
||||
if (registeredVersion) {
|
||||
indexStats.jobs++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Job } from "@trigger.dev/database";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { telemetry } from "../telemetry.server";
|
||||
|
||||
export class DeleteJobService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(job: Job) {
|
||||
// Make sure that all the latest versions are disabled
|
||||
const latestVersions = await this.#prismaClient.jobAlias.findMany({
|
||||
where: {
|
||||
jobId: job.id,
|
||||
name: "latest",
|
||||
},
|
||||
include: {
|
||||
version: true,
|
||||
},
|
||||
});
|
||||
|
||||
const allDisabled = latestVersions.every((alias) => alias.version.status === "DISABLED");
|
||||
|
||||
if (!allDisabled) {
|
||||
throw new Error("All latest versions must be disabled before deleting a job");
|
||||
}
|
||||
|
||||
// Okay now we need to delete a job by setting the deletedAt field and enqueuing a job to cleanup the job
|
||||
await this.#prismaClient.job.update({
|
||||
where: {
|
||||
id: job.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
telemetry.project.deletedJob({
|
||||
job,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,21 @@ export class RegisterJobService {
|
||||
endpoint: Endpoint,
|
||||
environment: AuthenticatedEnvironment,
|
||||
metadata: JobMetadata
|
||||
): Promise<JobVersion> {
|
||||
): Promise<JobVersion | undefined> {
|
||||
// Check the job doesn't already exist and is deleted
|
||||
const existingJob = await this.#prismaClient.job.findUnique({
|
||||
where: {
|
||||
projectId_slug: {
|
||||
projectId: environment.projectId,
|
||||
slug: metadata.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingJob && existingJob.deletedAt && !metadata.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = new Map<string, Integration>();
|
||||
|
||||
for (const [, jobIntegration] of Object.entries(metadata.integrations)) {
|
||||
@@ -155,6 +169,7 @@ export class RegisterJobService {
|
||||
},
|
||||
update: {
|
||||
title: metadata.name,
|
||||
deletedAt: metadata.enabled ? null : undefined,
|
||||
},
|
||||
include: {
|
||||
integrations: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Job } from "@trigger.dev/database";
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
import { PostHog } from "posthog-node";
|
||||
import { env } from "~/env.server";
|
||||
@@ -149,6 +150,14 @@ class Telemetry {
|
||||
},
|
||||
});
|
||||
},
|
||||
deletedJob: ({ job }: { job: Job }) => {
|
||||
this.#triggerClient?.sendEvent({
|
||||
name: "job.deleted",
|
||||
payload: {
|
||||
id: job.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
#capture(event: CaptureEvent) {
|
||||
|
||||
@@ -115,6 +115,10 @@ export function projectPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return `/orgs/${organizationParam(organization)}/projects/${projectParam(project)}`;
|
||||
}
|
||||
|
||||
export function projectJobsPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return projectPath(organization, project);
|
||||
}
|
||||
|
||||
export function projectIntegrationsPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return `${projectPath(organization, project)}/integrations`;
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ module.exports = {
|
||||
},
|
||||
devEnv: colors.pink,
|
||||
liveEnv: colors.green,
|
||||
uiBorder: slate[800],
|
||||
},
|
||||
borderRadius: {
|
||||
lg: radius,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: "Managing Jobs"
|
||||
description: "Managing jobs in your codebase and the dashboard"
|
||||
---
|
||||
|
||||
## Disabling jobs
|
||||
|
||||
To prevent a job from processing new runs, you can disable it by setting the `enabled` option:
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({ name: "example.event" }),
|
||||
enabled: false,
|
||||
run: async (payload, io, ctx) => {
|
||||
// your job code here
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you omit the `enabled` option, it will default to `true`.
|
||||
|
||||
The job will only be disabled in environments that have seen the `enabled = false` value. So the job will remain enabled in production until the code with the `enabled = false` is deployed to production.
|
||||
|
||||
<Note>
|
||||
Currently this is the only way to disable a job. If you'd like to disable a job in the Dashboard,
|
||||
please reach out to us on [Discord](https://discord.gg/kA47vcd8P6) and let us know 👋
|
||||
</Note>
|
||||
|
||||
Once a job is disabled no **new** runs will be created for that job, and it will still be visible in the Dashboard as disabled:
|
||||
|
||||

|
||||
|
||||
### In-progress runs
|
||||
|
||||
In-progress runs will be allowed to finish, even runs that are currently delayed from a call to `io.wait`. If you'd like to completely stop in-progress runs, you have two options:
|
||||
|
||||
- Set the `enabled` option to false and then `throw` an error at the top of your job `run` function.
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({ name: "example.event" }),
|
||||
enabled: false,
|
||||
run: async (payload, io, ctx) => {
|
||||
throw new Error("Job disabled");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- Delete the job from your codebase. This will disable the job as well but also stop in progress runs.
|
||||
|
||||
### Disabling in production with env vars
|
||||
|
||||
You can easily disable jobs in production using env vars so you don't have to deploy new code to disable a job.
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.1.0",
|
||||
trigger: eventTrigger({ name: "example.event" }),
|
||||
enabled: process.env.TRIGGER_JOBS_DISABLED === "true",
|
||||
run: async (payload, io, ctx) => {
|
||||
// your job code here
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then you can disable the job in production by setting the `TRIGGER_JOBS_DISABLED` env var to `"true"`. And removing the env var will re-enable the job.
|
||||
|
||||
## Deleting jobs
|
||||
|
||||
Once you have disabled a job in all environments, you can delete it from the dashboard by navigating to the Job list page and clicking the "triple-dot" menu next to the job you want to delete:
|
||||
|
||||

|
||||
|
||||
This will bring up a dialog confirming that you want to delete the job and all of its history:
|
||||
|
||||

|
||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
+34
-11
@@ -63,7 +63,10 @@
|
||||
"documentation/introduction",
|
||||
{
|
||||
"group": "Quick Starts",
|
||||
"pages": ["documentation/quickstarts/nextjs", "documentation/quickstarts/supabase"]
|
||||
"pages": [
|
||||
"documentation/quickstarts/nextjs",
|
||||
"documentation/quickstarts/supabase"
|
||||
]
|
||||
},
|
||||
"documentation/guides/create-a-job",
|
||||
"documentation/guides/video-walkthrough"
|
||||
@@ -114,6 +117,7 @@
|
||||
"documentation/guides/cli",
|
||||
"documentation/guides/manual",
|
||||
"documentation/guides/running-jobs",
|
||||
"documentation/guides/jobs/managing",
|
||||
{
|
||||
"group": "Using the Dashboard",
|
||||
"pages": [
|
||||
@@ -167,7 +171,10 @@
|
||||
},
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["integrations/introduction", "integrations/create"]
|
||||
"pages": [
|
||||
"integrations/introduction",
|
||||
"integrations/create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
@@ -180,23 +187,30 @@
|
||||
"integrations/apis/github-tasks"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"group": "OpenAI",
|
||||
"pages": ["integrations/apis/openai"]
|
||||
"pages": [
|
||||
"integrations/apis/openai"
|
||||
]
|
||||
},
|
||||
"integrations/apis/plain",
|
||||
{
|
||||
"group": "Resend",
|
||||
"pages": ["integrations/apis/resend"]
|
||||
"pages": [
|
||||
"integrations/apis/resend"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "SendGrid",
|
||||
"pages": ["integrations/apis/sendgrid"]
|
||||
"pages": [
|
||||
"integrations/apis/sendgrid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Slack",
|
||||
"pages": ["integrations/apis/slack"]
|
||||
"pages": [
|
||||
"integrations/apis/slack"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Supabase",
|
||||
@@ -258,7 +272,10 @@
|
||||
"sdk/dynamictrigger/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
|
||||
"pages": [
|
||||
"sdk/dynamictrigger/register",
|
||||
"sdk/dynamictrigger/unregister"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -269,7 +286,10 @@
|
||||
"sdk/dynamicschedule/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
|
||||
"pages": [
|
||||
"sdk/dynamicschedule/register",
|
||||
"sdk/dynamicschedule/unregister"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -290,7 +310,10 @@
|
||||
},
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["examples/introduction", "examples/examples-repository"]
|
||||
"pages": [
|
||||
"examples/introduction",
|
||||
"examples/examples-repository"
|
||||
]
|
||||
}
|
||||
],
|
||||
"footerSocials": {
|
||||
@@ -303,4 +326,4 @@
|
||||
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-21
@@ -49,27 +49,6 @@ client.defineJob({
|
||||
});
|
||||
```
|
||||
|
||||
```ts queue options
|
||||
client.defineJob({
|
||||
id: "github-integration-on-issue",
|
||||
name: "GitHub Integration - On Issue",
|
||||
version: "0.1.0",
|
||||
trigger: github.triggers.repo({
|
||||
event: events.onIssue,
|
||||
owner: "triggerdotdev",
|
||||
repo: "empty",
|
||||
}),
|
||||
queue: {
|
||||
name: "my-queue",
|
||||
maxConcurrent: 10, // only 10 runs can happen at the same time
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("This is a simple log info message");
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</RequestExample>
|
||||
|
||||
# Constructor
|
||||
@@ -114,6 +93,9 @@ client.defineJob({
|
||||
Imports the specified integrations into the Job. The integrations will be available on the `io` object in the `run()` function with the same name as the key. For example:
|
||||
<Snippet file="how-to-pass-integrations.mdx" />
|
||||
</ParamField>
|
||||
<ParamField body="enabled" type="boolean">
|
||||
The `enabled` property is an optional property that specifies whether the Job is enabled or not. The Job will be enabled by default if you omit this property. When a job is disabled, no new runs will be triggered or resumed. In progress runs will continue to run until they are finished or delayed by using `io.wait`.
|
||||
</ParamField>
|
||||
<ParamField body="logLevel" type="log | error | warn | info | debug">
|
||||
The `logLevel` property is an optional property that specifies the level of
|
||||
logging for the Job. The level is inherited from the client if you omit this property.
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Job" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||
@@ -420,6 +420,8 @@ model Job {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
deletedAt DateTime?
|
||||
|
||||
@@unique([projectId, slug])
|
||||
}
|
||||
|
||||
|
||||
@@ -605,15 +605,6 @@ export class TriggerClient {
|
||||
}
|
||||
|
||||
async #executeJob(body: RunJobBody, job: Job<Trigger<any>, any>): Promise<RunJobResponse> {
|
||||
if (!job.enabled) {
|
||||
return {
|
||||
status: "ERROR",
|
||||
error: {
|
||||
message: "Job is disabled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
this.#internalLogger.debug("executing job", {
|
||||
execution: body,
|
||||
job: job.toJSON(),
|
||||
|
||||
Reference in New Issue
Block a user