The run detail column is now a separate outlet with event and task routes
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { useLocation, useNavigation } from "@remix-run/react";
|
||||
|
||||
export function usePathName(preemptive = true) {
|
||||
const navigation = useNavigation();
|
||||
const location = useLocation();
|
||||
|
||||
if (!preemptive || navigation.state === "idle" || !navigation.location) {
|
||||
return location.pathname;
|
||||
}
|
||||
|
||||
return navigation.location.pathname;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
DisplayElementSchema,
|
||||
StyleSchema,
|
||||
} from "@/../../packages/internal/src";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type DetailedEvent = NonNullable<
|
||||
Awaited<ReturnType<EventDetailsPresenter["call"]>>
|
||||
>;
|
||||
|
||||
export class EventDetailsPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({ id, userId }: DetailsProps) {
|
||||
const event = await this.#prismaClient.eventRecord.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
}
|
||||
@@ -27,42 +27,31 @@ const ElementsSchema = z.array(DisplayElementSchema);
|
||||
const taskSelect = {
|
||||
id: true,
|
||||
displayKey: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
status: true,
|
||||
delayUntil: true,
|
||||
description: true,
|
||||
elements: true,
|
||||
error: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
runConnection: {
|
||||
select: {
|
||||
id: true,
|
||||
key: true,
|
||||
apiConnection: {
|
||||
select: {
|
||||
metadata: true,
|
||||
connectionType: true,
|
||||
client: {
|
||||
select: {
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
scopes: true,
|
||||
integrationIdentifier: true,
|
||||
integrationAuthMethod: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
icon: true,
|
||||
status: true,
|
||||
delayUntil: true,
|
||||
noop: true,
|
||||
description: true,
|
||||
elements: true,
|
||||
params: true,
|
||||
output: true,
|
||||
error: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
} as const;
|
||||
|
||||
export class RunPresenter {
|
||||
@@ -99,7 +88,6 @@ export class RunPresenter {
|
||||
return {
|
||||
...t,
|
||||
connection: t.runConnection,
|
||||
params: t.params as Record<string, any>,
|
||||
elements:
|
||||
t.elements == null
|
||||
? []
|
||||
@@ -246,15 +234,4 @@ export class RunPresenter {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// #recursivelyEnrichTask(
|
||||
// task: QueryTask
|
||||
// ): EnrichedTask & { subtasks: EnrichedTask[] } {
|
||||
// const enrichedTask = this.enrichTask(task);
|
||||
// let subtasks: (EnrichedTask & { subtasks: EnrichedTask[] })[] = [];
|
||||
// if (task.children) {
|
||||
// subtasks = task.children.map((t) => this.#recursivelyEnrichTask(t));
|
||||
// }
|
||||
// return { ...enrichedTask, subtasks };
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
DisplayElementSchema,
|
||||
StyleSchema,
|
||||
} from "@/../../packages/internal/src";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type DetailedTask = NonNullable<
|
||||
Awaited<ReturnType<TaskDetailsPresenter["call"]>>
|
||||
>;
|
||||
|
||||
export class TaskDetailsPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({ id, userId }: DetailsProps) {
|
||||
const task = await this.#prismaClient.task.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
displayKey: true,
|
||||
runConnection: {
|
||||
select: {
|
||||
id: true,
|
||||
key: true,
|
||||
apiConnection: {
|
||||
select: {
|
||||
metadata: true,
|
||||
connectionType: true,
|
||||
client: {
|
||||
select: {
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
scopes: true,
|
||||
integrationIdentifier: true,
|
||||
integrationAuthMethod: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
icon: true,
|
||||
status: true,
|
||||
delayUntil: true,
|
||||
noop: true,
|
||||
description: true,
|
||||
elements: true,
|
||||
params: true,
|
||||
output: true,
|
||||
error: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
elements:
|
||||
task.elements == null
|
||||
? []
|
||||
: z.array(DisplayElementSchema).parse(task.elements),
|
||||
style: task.style ? StyleSchema.parse(task.style) : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
|
||||
export default function Page() {
|
||||
<Paragraph>Nothing selected</Paragraph>;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelHeader,
|
||||
RunPanelIconElement,
|
||||
RunPanelIconSection,
|
||||
} from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/RunCard";
|
||||
import { EventDetailsPresenter } from "~/presenters/EventDetailsPresenter.server";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { jobParam, runParam, eventParam } = params;
|
||||
invariant(jobParam, "jobParam not found");
|
||||
invariant(runParam, "runParam not found");
|
||||
invariant(eventParam, "eventParam not found");
|
||||
|
||||
const presenter = new EventDetailsPresenter();
|
||||
const event = await presenter.call({
|
||||
userId,
|
||||
id: eventParam,
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Response(null, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { event } = useTypedLoaderData<typeof loader>();
|
||||
const job = useJob();
|
||||
|
||||
const { id, name, payload, timestamp, deliveredAt } = event;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelHeader icon={job.event.icon} title={job.event.title} />
|
||||
<RunPanelBody>
|
||||
<div className="mb-4 border-b border-slate-800 pb-4">
|
||||
<RunPanelIconSection>
|
||||
<RunPanelIconElement
|
||||
icon="calendar"
|
||||
label="Created"
|
||||
value={formatDateTime(timestamp, "long")}
|
||||
/>
|
||||
{deliveredAt && (
|
||||
<RunPanelIconElement
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(deliveredAt, "long")}
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconElement icon="id" label="Event name" value={name} />
|
||||
</RunPanelIconSection>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={JSON.stringify(payload, null, 2)} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
+42
-72
@@ -1,7 +1,11 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Event, Task } from "~/presenters/RunPresenter.server";
|
||||
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
@@ -12,43 +16,48 @@ import {
|
||||
RunPanelIconElement,
|
||||
RunPanelIconSection,
|
||||
RunPanelIconTitle,
|
||||
} from "./RunCard";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
} from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/RunCard";
|
||||
import { TaskStatusIcon } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/TaskStatus";
|
||||
|
||||
type Trigger = Event & { icon: string; title: string };
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { jobParam, runParam, taskParam } = params;
|
||||
invariant(jobParam, "jobParam not found");
|
||||
invariant(runParam, "runParam not found");
|
||||
invariant(taskParam, "selectedParam not found");
|
||||
|
||||
type DetailProps =
|
||||
| {
|
||||
type: "task";
|
||||
task: Task;
|
||||
}
|
||||
| {
|
||||
type: "trigger";
|
||||
trigger: Trigger;
|
||||
};
|
||||
const presenter = new TaskDetailsPresenter();
|
||||
const task = await presenter.call({
|
||||
userId,
|
||||
id: taskParam,
|
||||
});
|
||||
|
||||
export function Detail(props: DetailProps) {
|
||||
switch (props.type) {
|
||||
case "task":
|
||||
return <TaskDetail {...props.task} />;
|
||||
case "trigger":
|
||||
return <EventDetail {...props.trigger} />;
|
||||
default:
|
||||
return <></>;
|
||||
if (!task) {
|
||||
throw new Response(null, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function TaskDetail({
|
||||
name,
|
||||
icon,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
delayUntil,
|
||||
params,
|
||||
elements,
|
||||
output,
|
||||
}: Task) {
|
||||
return typedjson({
|
||||
task,
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { task } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
const {
|
||||
name,
|
||||
icon,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
delayUntil,
|
||||
params,
|
||||
elements,
|
||||
output,
|
||||
} = task;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelHeader
|
||||
@@ -124,42 +133,3 @@ export function TaskDetail({
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventDetail({
|
||||
icon,
|
||||
title,
|
||||
id,
|
||||
name,
|
||||
payload,
|
||||
timestamp,
|
||||
deliveredAt,
|
||||
}: Trigger) {
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelHeader icon={icon} title={title} />
|
||||
<RunPanelBody>
|
||||
<div className="mb-4 border-b border-slate-800 pb-4">
|
||||
<RunPanelIconSection>
|
||||
<RunPanelIconElement
|
||||
icon="calendar"
|
||||
label="Created"
|
||||
value={formatDateTime(timestamp, "long")}
|
||||
/>
|
||||
{deliveredAt && (
|
||||
<RunPanelIconElement
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(deliveredAt, "long")}
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconElement icon="id" label="Event name" value={name} />
|
||||
</RunPanelIconSection>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={JSON.stringify(payload, null, 2)} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
+5
-4
@@ -22,14 +22,15 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
type TaskCardProps = Task & {
|
||||
selectedId?: string;
|
||||
setSelectedId: (id: string) => void;
|
||||
selectedTask: (id: string) => void;
|
||||
isLast: boolean;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
//todo add links to elements
|
||||
export function TaskCard({
|
||||
selectedId,
|
||||
setSelectedId,
|
||||
selectedTask,
|
||||
isLast,
|
||||
depth,
|
||||
id,
|
||||
@@ -53,7 +54,7 @@ export function TaskCard({
|
||||
<div style={{ marginLeft: `${depth}rem` }}>
|
||||
<RunPanel
|
||||
selected={isSelected}
|
||||
onClick={() => setSelectedId(id)}
|
||||
onClick={() => selectedTask(id)}
|
||||
styleName={style?.style}
|
||||
>
|
||||
<RunPanelHeader
|
||||
@@ -158,7 +159,7 @@ export function TaskCard({
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 100 }}>
|
||||
<TaskCard
|
||||
selectedId={selectedId}
|
||||
setSelectedId={setSelectedId}
|
||||
selectedTask={selectedTask}
|
||||
isLast={index === subtasks.length - 1}
|
||||
depth={depth + 1}
|
||||
{...subtask}
|
||||
|
||||
+62
-45
@@ -1,7 +1,13 @@
|
||||
import { BoltIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
} from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -21,30 +27,35 @@ import { RunStatusIcon, runStatusTitle } from "~/components/runs/RunStatuses";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunPresenter, Task } from "~/presenters/RunPresenter.server";
|
||||
import { RunPresenter } from "~/presenters/RunPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { jobPath } from "~/utils/pathBuilder";
|
||||
import { Detail } from "./DetailView";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDescription,
|
||||
RunPanelElements,
|
||||
RunPanelHeader,
|
||||
RunPanelIconElement,
|
||||
RunPanelIconSection,
|
||||
RunPanelIconTitle,
|
||||
TaskSeparator,
|
||||
} from "./RunCard";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { taskPath, eventPath } from "~/utils/pathBuilder";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { jobParam, runParam } = params;
|
||||
const {
|
||||
organizationSlug,
|
||||
projectParam,
|
||||
jobParam,
|
||||
runParam,
|
||||
eventParam,
|
||||
taskParam,
|
||||
} = params;
|
||||
invariant(organizationSlug, "organizationSlug not found");
|
||||
invariant(projectParam, "projectParam not found");
|
||||
invariant(jobParam, "jobParam not found");
|
||||
invariant(runParam, "runParam not found");
|
||||
|
||||
@@ -60,6 +71,18 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!eventParam && !taskParam) {
|
||||
return redirect(
|
||||
eventPath(
|
||||
{ slug: organizationSlug },
|
||||
{ slug: projectParam },
|
||||
{ id: jobParam },
|
||||
{ id: runParam },
|
||||
run.event.id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
run,
|
||||
});
|
||||
@@ -71,39 +94,37 @@ export const handle: Handle = {
|
||||
},
|
||||
};
|
||||
|
||||
const taskPattern = /\/tasks\/(.*)/;
|
||||
const eventPattern = /\/events\/(.*)/;
|
||||
|
||||
export default function Page() {
|
||||
const { run } = useTypedLoaderData<typeof loader>();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
const [selectedId, setSelectedId] = useState<string | undefined>(
|
||||
run.event.id
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const flatTasks = useMemo(() => {
|
||||
const tasks: Task[] = [];
|
||||
const queue: Task[] = [...run.tasks];
|
||||
while (queue.length > 0) {
|
||||
const task = queue.shift();
|
||||
if (!task) continue;
|
||||
tasks.push(task);
|
||||
if (task.subtasks) {
|
||||
queue.push(...task.subtasks);
|
||||
}
|
||||
const selectedTask = useCallback((id: string) => {
|
||||
navigate(taskPath(organization, project, job, run, id));
|
||||
}, []);
|
||||
|
||||
const selectedEvent = useCallback((id: string) => {
|
||||
navigate(eventPath(organization, project, job, run, id));
|
||||
}, []);
|
||||
|
||||
const pathName = usePathName();
|
||||
|
||||
const selectedId = useMemo(() => {
|
||||
const taskMatch = pathName.match(taskPattern);
|
||||
const taskId = taskMatch ? taskMatch[1] : undefined;
|
||||
if (taskId) {
|
||||
return taskId;
|
||||
}
|
||||
return tasks;
|
||||
}, [run]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
if (!selectedId) return undefined;
|
||||
if (selectedId === run.event.id)
|
||||
return {
|
||||
type: "trigger" as const,
|
||||
trigger: { ...run.event, icon: job.event.icon, title: job.event.title },
|
||||
};
|
||||
const task = flatTasks.find((task) => task.id === selectedId);
|
||||
if (task) return { type: "task" as const, task };
|
||||
}, [selectedId, run]);
|
||||
const eventMatch = pathName.match(eventPattern);
|
||||
const eventId = eventMatch ? eventMatch[1] : undefined;
|
||||
return eventId;
|
||||
}, [pathName]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -179,7 +200,7 @@ export default function Page() {
|
||||
<Header2 className="mb-2">Trigger</Header2>
|
||||
<RunPanel
|
||||
selected={run.event.id === selectedId}
|
||||
onClick={() => setSelectedId(run.event.id)}
|
||||
onClick={() => selectedEvent(run.event.id)}
|
||||
>
|
||||
<RunPanelHeader
|
||||
icon={<BoltIcon className="h-5 w-5 text-orange-500" />}
|
||||
@@ -222,7 +243,7 @@ export default function Page() {
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
selectedId={selectedId}
|
||||
setSelectedId={setSelectedId}
|
||||
selectedTask={selectedTask}
|
||||
isLast={isLast}
|
||||
depth={0}
|
||||
{...task}
|
||||
@@ -234,14 +255,10 @@ export default function Page() {
|
||||
{/* Detail view */}
|
||||
<div className="overflow-y-auto py-4 pr-4">
|
||||
<Header2 className="mb-2">Detail</Header2>
|
||||
{!selectedItem ? (
|
||||
<RunPanel selected={false} className="h-full">
|
||||
<Paragraph variant="base" className="p-4">
|
||||
Nothing selected
|
||||
</Paragraph>
|
||||
</RunPanel>
|
||||
{selectedId ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Detail {...selectedItem} />
|
||||
<Callout variant="info">Select a task or trigger</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,6 +152,28 @@ export function runParam(run: RunForPath) {
|
||||
return run.id;
|
||||
}
|
||||
|
||||
// Task
|
||||
export function taskPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
run: RunForPath,
|
||||
taskId: string
|
||||
) {
|
||||
return `${runPath(organization, project, job, run)}/tasks/${taskId}`;
|
||||
}
|
||||
|
||||
// Event
|
||||
export function eventPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
run: RunForPath,
|
||||
eventId: string
|
||||
) {
|
||||
return `${runPath(organization, project, job, run)}/events/${eventId}`;
|
||||
}
|
||||
|
||||
// Docs
|
||||
const docsRoot = "https://docs.trigger.dev";
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ export const DisplayElementSchema = z.object({
|
||||
url: z.string().optional(),
|
||||
});
|
||||
|
||||
export const DisplayElementsSchema = z.array(DisplayElementSchema);
|
||||
|
||||
export type DisplayElement = z.infer<typeof DisplayElementSchema>;
|
||||
|
||||
export const StyleSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user