Files
triggerdotdev--trigger.dev/apps/webapp/app/components/schedules/ScheduleInspector.tsx
T
2026-07-13 13:32:57 +08:00

353 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
BoltIcon,
BoltSlashIcon,
BookOpenIcon,
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/20/solid";
import { DialogDescription } from "@radix-ui/react-dialog";
import { type FetcherWithComponents, Form, useLocation } from "@remix-run/react";
import { type ReactNode } from "react";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { InfoPanel } from "~/components/primitives/InfoPanel";
import { Paragraph } from "~/components/primitives/Paragraph";
import * as Property from "~/components/primitives/PropertyTable";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
import { ScheduleTypeCombo } from "~/components/runs/v3/ScheduleType";
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { v3EditSchedulePath } from "~/utils/pathBuilder";
type RunRow = React.ComponentProps<typeof TaskRunsTable>["runs"][number];
type EnvironmentRow = React.ComponentProps<typeof EnvironmentCombo>["environment"] & {
id: string;
};
export type ScheduleInspectorData = {
id: string;
friendlyId: string;
type: "DECLARATIVE" | "IMPERATIVE";
taskIdentifier: string;
cron: string;
cronDescription: string;
timezone: string;
externalId: string | null;
deduplicationKey: string | null;
userProvidedDeduplicationKey: boolean;
active: boolean;
environments: EnvironmentRow[];
runs: RunRow[];
nextRuns: Date[];
};
type Props = {
schedule: ScheduleInspectorData;
/**
* Right-aligned slot in the header (e.g. a back link or close button).
*/
headerActions?: ReactNode;
/**
* URL the action `Form`s post to. Defaults to the current page (Form's
* default behavior) — pass the schedule detail route when the inspector
* is rendered somewhere else (e.g. in a sheet on a different page).
*/
actionPath?: string;
/** When set, Edit calls back instead of navigating to the standalone edit page. */
onEdit?: () => void;
/** Submits enable/disable via this fetcher with `_format=json` so the host stays put. */
activeToggleFetcher?: FetcherWithComponents<unknown>;
/** Submits delete via this fetcher with `_format=json` so the host stays put. */
deleteFetcher?: FetcherWithComponents<unknown>;
};
export function ScheduleInspector({
schedule,
headerActions,
actionPath,
onEdit,
activeToggleFetcher,
deleteFetcher,
}: Props) {
const location = useLocation();
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const isUtc = schedule.timezone === "UTC";
const isImperative = schedule.type === "IMPERATIVE";
return (
<div
className={cn(
"grid h-full max-h-full overflow-hidden bg-background-bright",
isImperative ? "grid-rows-[2.5rem_1fr_auto]" : "grid-rows-[2.5rem_1fr]"
)}
>
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
<Header2 className="whitespace-nowrap">{schedule.friendlyId}</Header2>
{headerActions}
</div>
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<div className="space-y-3">
<div className="p-3">
<Property.Table>
<Property.Item>
<Property.Label>Schedule ID</Property.Label>
<Property.Value>{schedule.friendlyId}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Task ID</Property.Label>
<Property.Value>{schedule.taskIdentifier}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Type</Property.Label>
<Property.Value>
<ScheduleTypeCombo type={schedule.type} className="text-sm" />
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>CRON</Property.Label>
<Property.Value>
<div className="space-y-2">
<InlineCode variant="extra-small">{schedule.cron}</InlineCode>
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
</div>
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Timezone</Property.Label>
<Property.Value>{schedule.timezone}</Property.Value>
</Property.Item>
<Property.Item className="gap-1">
<Property.Label>Environment</Property.Label>
<Property.Value>
<div className="flex flex-col gap-2">
{schedule.environments.map((env) => (
<EnvironmentCombo key={env.id} environment={env} className="text-xs" />
))}
</div>
</Property.Value>
</Property.Item>
{isImperative && (
<>
<Property.Item>
<Property.Label>External ID</Property.Label>
<Property.Value>
{schedule.externalId ? schedule.externalId : ""}
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Deduplication key</Property.Label>
<Property.Value>
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
</Property.Value>
</Property.Item>
<Property.Item className="gap-1.5">
<Property.Label>Status</Property.Label>
<Property.Value>
<EnabledStatus enabled={schedule.active} />
</Property.Value>
</Property.Item>
</>
)}
</Property.Table>
</div>
<div className="flex flex-col gap-1">
<Header3 className="pb-1 pl-3">Last 5 runs</Header3>
<TaskRunsTable
total={schedule.runs.length}
hasFilters={false}
filters={{
tasks: [],
versions: [],
statuses: [],
from: undefined,
to: undefined,
}}
runs={schedule.runs}
isLoading={false}
variant="bright"
disableAdjacentRows
/>
</div>
<div className="flex flex-col gap-1 pt-2">
<Header3 className="pb-1 pl-3">Next 5 runs</Header3>
<Table variant="bright">
<TableHeader>
<TableRow>
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{schedule.active ? (
schedule.nextRuns.length ? (
schedule.nextRuns.map((run, index) => (
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
</TableCell>
</TableRow>
))
) : (
<TableBlankRow colSpan={isUtc ? 1 : 2}>
<PlaceholderText title="You found a bug" />
</TableBlankRow>
)
) : (
<TableBlankRow colSpan={isUtc ? 1 : 2}>
<PlaceholderText title="Schedule disabled" />
</TableBlankRow>
)}
</TableBody>
</Table>
</div>
{!isImperative && (
<div className="p-3">
<InfoPanel
title="Editing declarative schedules"
icon={BookOpenIcon}
iconClassName="text-indigo-500"
variant="info"
accessory={
<LinkButton
to="https://trigger.dev/docs/v3/tasks-scheduled"
variant="docs/small"
LeadingIcon={BookOpenIcon}
>
Schedules docs
</LinkButton>
}
panelClassName="max-w-full"
>
You can only edit a declarative schedule by updating your schedules.task and then
running the CLI dev and deploy commands.
</InfoPanel>
</div>
)}
</div>
</div>
{isImperative && (
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2 py-2">
<div className="flex items-center gap-2">
{(() => {
const ToggleForm = activeToggleFetcher?.Form ?? Form;
const isSubmitting = activeToggleFetcher?.state === "submitting";
return (
<ToggleForm method="post" action={actionPath}>
{activeToggleFetcher ? <input type="hidden" name="_format" value="json" /> : null}
<Button
type="submit"
variant="secondary/small"
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
name="action"
value={schedule.active ? "disable" : "enable"}
disabled={isSubmitting}
>
{schedule.active ? "Disable" : "Enable"}
</Button>
</ToggleForm>
);
})()}
<Dialog>
<DialogTrigger asChild>
<Button
type="submit"
variant="danger/small"
LeadingIcon={TrashIcon}
name="action"
value="delete"
>
Delete
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-sm">
<DialogHeader>Delete schedule</DialogHeader>
<DialogDescription className="mt-3">
Are you sure you want to delete this schedule? This can't be reversed.
</DialogDescription>
<DialogFooter className="sm:justify-end">
{(() => {
const DeleteForm = deleteFetcher?.Form ?? Form;
const isSubmitting = deleteFetcher?.state === "submitting";
return (
<DeleteForm method="post" action={actionPath}>
{deleteFetcher ? <input type="hidden" name="_format" value="json" /> : null}
<Button
type="submit"
variant="danger/medium"
LeadingIcon={TrashIcon}
name="action"
value="delete"
disabled={isSubmitting}
>
Delete
</Button>
</DeleteForm>
);
})()}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="flex items-center gap-4">
{onEdit ? (
<Button variant="secondary/small" LeadingIcon={PencilSquareIcon} onClick={onEdit}>
Edit schedule
</Button>
) : (
<LinkButton
variant="secondary/small"
to={`${v3EditSchedulePath(organization, project, environment, schedule)}${
location.search
}`}
LeadingIcon={PencilSquareIcon}
>
Edit schedule
</LinkButton>
)}
</div>
</div>
)}
</div>
);
}
function PlaceholderText({ title }: { title: string }) {
return (
<div className="flex items-center justify-center">
<Paragraph className="w-auto">{title}</Paragraph>
</div>
);
}