diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx
index 09071bac7..de3f33ac1 100644
--- a/apps/webapp/app/components/primitives/NamedIcon.tsx
+++ b/apps/webapp/app/components/primitives/NamedIcon.tsx
@@ -221,6 +221,9 @@ const icons = {
"custom-event": (className: string) => (
),
+ "register-source": (className: string) => (
+
+ ),
"schedule-interval": (className: string) => (
),
diff --git a/apps/webapp/app/components/run/RunCompletedDetail.tsx b/apps/webapp/app/components/run/RunCompletedDetail.tsx
index 839b99bf5..ed85bb515 100644
--- a/apps/webapp/app/components/run/RunCompletedDetail.tsx
+++ b/apps/webapp/app/components/run/RunCompletedDetail.tsx
@@ -2,7 +2,7 @@ import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
-import { useRun } from "~/hooks/useRun";
+import { MatchedRun, useRun } from "~/hooks/useRun";
import { formatDuration } from "~/utils";
import {
RunPanel,
@@ -14,9 +14,7 @@ import {
RunPanelIconSection,
} from "./RunCard";
-export function RunCompletedDetail() {
- const run = useRun();
-
+export function RunCompletedDetail({ run }: { run: MatchedRun }) {
return (
;
+ const run = useRun();
+
+ return ;
}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.completed/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.completed/route.tsx
new file mode 100644
index 000000000..bd2788457
--- /dev/null
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.completed/route.tsx
@@ -0,0 +1,20 @@
+import { useTypedRouteLoaderData } from "remix-typedjson";
+import { RunCompletedDetail } from "~/components/run/RunCompletedDetail";
+import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam/route";
+
+function useTriggerRegisterRun() {
+ const routeMatch = useTypedRouteLoaderData(
+ "routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam"
+ );
+
+ if (!routeMatch || !routeMatch.run) {
+ throw new Error("No run found");
+ }
+
+ return routeMatch.run;
+}
+
+export default function RunCompletedPage() {
+ const run = useTriggerRegisterRun();
+ return ;
+}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.tasks.$taskParam/route.tsx
new file mode 100644
index 000000000..d2a118146
--- /dev/null
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.tasks.$taskParam/route.tsx
@@ -0,0 +1,32 @@
+import { LoaderArgs } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { TaskDetail } from "~/components/run/TaskDetail";
+import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
+import { requireUserId } from "~/services/session.server";
+import { TriggerSourceRunTaskParamsSchema } from "~/utils/pathBuilder";
+
+export const loader = async ({ request, params }: LoaderArgs) => {
+ const userId = await requireUserId(request);
+ const { taskParam } = TriggerSourceRunTaskParamsSchema.parse(params);
+
+ const presenter = new TaskDetailsPresenter();
+ const task = await presenter.call({
+ userId,
+ id: taskParam,
+ });
+
+ if (!task) {
+ throw new Response(null, {
+ status: 404,
+ });
+ }
+
+ return typedjson({
+ task,
+ });
+};
+
+export default function Page() {
+ const { task } = useTypedLoaderData();
+ return ;
+}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.trigger/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.trigger/route.tsx
new file mode 100644
index 000000000..89298f9f5
--- /dev/null
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam.trigger/route.tsx
@@ -0,0 +1,34 @@
+import { LoaderArgs } from "@remix-run/server-runtime";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { TriggerDetail } from "~/components/run/TriggerDetail";
+import { TriggerDetailsPresenter } from "~/presenters/TriggerDetailsPresenter.server";
+import { TriggerSourceRunParamsSchema } from "~/utils/pathBuilder";
+
+export const loader = async ({ request, params }: LoaderArgs) => {
+ const { runParam } = TriggerSourceRunParamsSchema.parse(params);
+
+ const presenter = new TriggerDetailsPresenter();
+ const trigger = await presenter.call(runParam);
+
+ if (!trigger) {
+ throw new Response(null, {
+ status: 404,
+ });
+ }
+
+ return typedjson({
+ trigger,
+ });
+};
+
+export default function Page() {
+ const { trigger } = useTypedLoaderData();
+
+ return (
+
+ );
+}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam/route.tsx
new file mode 100644
index 000000000..abd65c7ad
--- /dev/null
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.triggersources.$triggerParam_.runs.$runParam/route.tsx
@@ -0,0 +1,80 @@
+import { useRevalidator } from "@remix-run/react";
+import { LoaderArgs } from "@remix-run/server-runtime";
+import { useEffect } from "react";
+import { typedjson, useTypedLoaderData } from "remix-typedjson";
+import { useEventSource } from "remix-utils";
+import { RunOverview } from "~/components/run/RunOverview";
+import { useOrganization } from "~/hooks/useOrganizations";
+import { useProject } from "~/hooks/useProject";
+import { RunPresenter } from "~/presenters/RunPresenter.server";
+import { requireUserId } from "~/services/session.server";
+import {
+ TriggerSourceRunParamsSchema,
+ triggerSourcePath,
+ triggerSourceRunPath,
+ triggerSourceRunStreamingPath,
+} from "~/utils/pathBuilder";
+
+export const loader = async ({ request, params }: LoaderArgs) => {
+ const userId = await requireUserId(request);
+ const { runParam, triggerParam } = TriggerSourceRunParamsSchema.parse(params);
+
+ const presenter = new RunPresenter();
+ const run = await presenter.call({
+ userId,
+ id: runParam,
+ });
+
+ if (!run) {
+ throw new Response(null, {
+ status: 404,
+ });
+ }
+
+ return typedjson({
+ run,
+ triggerParam,
+ });
+};
+
+export default function Page() {
+ const { run, triggerParam } = useTypedLoaderData();
+ const organization = useOrganization();
+ const project = useProject();
+
+ const revalidator = useRevalidator();
+ const events = useEventSource(
+ triggerSourceRunStreamingPath(
+ organization,
+ project,
+ { id: triggerParam },
+ run
+ ),
+ {
+ event: "message",
+ }
+ );
+ useEffect(() => {
+ if (events !== null) {
+ revalidator.revalidate();
+ }
+ // WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
+ }, [events]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+
+ );
+}
diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts
index fac248a05..bd8e1935f 100644
--- a/apps/webapp/app/utils/pathBuilder.ts
+++ b/apps/webapp/app/utils/pathBuilder.ts
@@ -39,6 +39,15 @@ export const TriggerSourceParamSchema = ProjectParamSchema.extend({
triggerParam: z.string(),
});
+export const TriggerSourceRunParamsSchema = TriggerSourceParamSchema.extend({
+ runParam: z.string(),
+});
+
+export const TriggerSourceRunTaskParamsSchema =
+ TriggerSourceRunParamsSchema.extend({
+ taskParam: z.string(),
+ });
+
export function organizationsPath() {
return `/`;
}
@@ -194,6 +203,24 @@ export function triggerSourceRunsPath(
return `${triggerSourcePath(organization, project, trigger)}/runs`;
}
+export function triggerSourceRunPath(
+ organization: OrgForPath,
+ project: ProjectForPath,
+ trigger: TriggerForPath,
+ run: RunForPath
+) {
+ return `${triggerSourceRunsPath(organization, project, trigger)}/${run.id}`;
+}
+
+export function triggerSourceRunStreamingPath(
+ organization: OrgForPath,
+ project: ProjectForPath,
+ trigger: TriggerForPath,
+ run: RunForPath
+) {
+ return `${triggerSourceRunPath(organization, project, trigger, run)}/stream`;
+}
+
function triggerSourceParam(trigger: TriggerForPath) {
return trigger.id;
}