Skip to content

Commit 33a9e1b

Browse files
committed
Merge branch 'main' into feature/tri-6738-show-aggregated-logs-in-main-page
2 parents b632a3c + ba9b0e1 commit 33a9e1b

18 files changed

Lines changed: 836 additions & 119 deletions

File tree

.changeset/quick-plums-tan.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Added support for idempotency reset

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ function TimelineView({
959959
const initialTimelineDimensions = useInitialDimensions(timelineContainerRef);
960960
const minTimelineWidth = initialTimelineDimensions?.width ?? 300;
961961
const maxTimelineWidth = minTimelineWidth * 10;
962+
const disableSpansAnimations = rootSpanStatus !== "executing";
962963

963964
//we want to live-update the duration if the root span is still executing
964965
const [duration, setDuration] = useState(queueAdjustedNs(totalDuration, queuedDuration));
@@ -1130,8 +1131,8 @@ function TimelineView({
11301131
"-ml-[0.5px] h-[0.5625rem] w-px rounded-none",
11311132
eventBackgroundClassName(node.data)
11321133
)}
1133-
layoutId={node.data.isPartial ? `${node.id}-${event.name}` : undefined}
1134-
animate={!node.data.isPartial ? false : undefined}
1134+
layoutId={disableSpansAnimations ? undefined : `${node.id}-${event.name}`}
1135+
animate={disableSpansAnimations ? false : undefined}
11351136
/>
11361137
)}
11371138
</Timeline.Point>
@@ -1149,8 +1150,8 @@ function TimelineView({
11491150
"-ml-[0.1562rem] size-[0.3125rem] rounded-full border bg-background-bright",
11501151
eventBorderClassName(node.data)
11511152
)}
1152-
layoutId={node.data.isPartial ? `${node.id}-${event.name}` : undefined}
1153-
animate={!node.data.isPartial ? false : undefined}
1153+
layoutId={disableSpansAnimations ? undefined : `${node.id}-${event.name}`}
1154+
animate={disableSpansAnimations ? false : undefined}
11541155
/>
11551156
)}
11561157
</Timeline.Point>
@@ -1169,8 +1170,8 @@ function TimelineView({
11691170
>
11701171
<motion.div
11711172
className={cn("h-px w-full", eventBackgroundClassName(node.data))}
1172-
layoutId={node.data.isPartial ? `mark-${node.id}` : undefined}
1173-
animate={!node.data.isPartial ? false : undefined}
1173+
layoutId={disableSpansAnimations ? undefined : `mark-${node.id}`}
1174+
animate={disableSpansAnimations ? false : undefined}
11741175
/>
11751176
</Timeline.Span>
11761177
) : null}
@@ -1193,6 +1194,7 @@ function TimelineView({
11931194
}
11941195
node={node}
11951196
fadeLeft={isTopSpan && queuedDuration !== undefined}
1197+
disableAnimations={disableSpansAnimations}
11961198
/>
11971199
</>
11981200
) : (
@@ -1207,8 +1209,8 @@ function TimelineView({
12071209
"-ml-0.5 size-3 rounded-full border-2 border-background-bright",
12081210
eventBackgroundClassName(node.data)
12091211
)}
1210-
layoutId={node.data.isPartial ? node.id : undefined}
1211-
animate={!node.data.isPartial ? false : undefined}
1212+
layoutId={disableSpansAnimations ? undefined : node.id}
1213+
animate={disableSpansAnimations ? false : undefined}
12121214
/>
12131215
)}
12141216
</Timeline.Point>
@@ -1440,8 +1442,9 @@ function SpanWithDuration({
14401442
showDuration,
14411443
node,
14421444
fadeLeft,
1445+
disableAnimations,
14431446
...props
1444-
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean; fadeLeft: boolean }) {
1447+
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean; fadeLeft: boolean; disableAnimations?: boolean }) {
14451448
return (
14461449
<Timeline.Span {...props}>
14471450
<motion.div
@@ -1451,8 +1454,8 @@ function SpanWithDuration({
14511454
fadeLeft ? "rounded-r-sm bg-gradient-to-r from-black/50 to-transparent" : "rounded-sm"
14521455
)}
14531456
style={{ backgroundSize: "20px 100%", backgroundRepeat: "no-repeat" }}
1454-
layoutId={node.data.isPartial ? node.id : undefined}
1455-
animate={!node.data.isPartial ? false : undefined}
1457+
layoutId={disableAnimations ? undefined : node.id}
1458+
animate={disableAnimations ? false : undefined}
14561459
>
14571460
{node.data.isPartial && (
14581461
<div
@@ -1465,12 +1468,12 @@ function SpanWithDuration({
14651468
"sticky left-0 z-10 transition-opacity group-hover:opacity-100",
14661469
!showDuration && "opacity-0"
14671470
)}
1468-
animate={!node.data.isPartial ? false : undefined}
1471+
animate={disableAnimations ? false : undefined}
14691472
>
14701473
<motion.div
14711474
className="whitespace-nowrap rounded-sm px-1 py-0.5 text-xxs text-text-bright text-shadow-custom"
1472-
layout={node.data.isPartial ? "position" : undefined}
1473-
animate={!node.data.isPartial ? false : undefined}
1475+
layout={disableAnimations ? undefined : "position"}
1476+
animate={disableAnimations ? false : undefined}
14741477
>
14751478
{formatDurationMilliseconds(props.durationMs, {
14761479
style: "short",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { json } from "@remix-run/server-runtime";
2+
import { ServiceValidationError } from "~/v3/services/baseService.server";
3+
import { z } from "zod";
4+
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
5+
import { ResetIdempotencyKeyService } from "~/v3/services/resetIdempotencyKey.server";
6+
import { logger } from "~/services/logger.server";
7+
8+
const ParamsSchema = z.object({
9+
key: z.string(),
10+
});
11+
12+
const BodySchema = z.object({
13+
taskIdentifier: z.string().min(1, "Task identifier is required"),
14+
});
15+
16+
export const { action } = createActionApiRoute(
17+
{
18+
params: ParamsSchema,
19+
body: BodySchema,
20+
allowJWT: true,
21+
corsStrategy: "all",
22+
authorization: {
23+
action: "write",
24+
resource: () => ({}),
25+
superScopes: ["write:runs", "admin"],
26+
},
27+
},
28+
async ({ params, body, authentication }) => {
29+
const service = new ResetIdempotencyKeyService();
30+
31+
try {
32+
const result = await service.call(
33+
params.key,
34+
body.taskIdentifier,
35+
authentication.environment
36+
);
37+
return json(result, { status: 200 });
38+
} catch (error) {
39+
if (error instanceof ServiceValidationError) {
40+
return json({ error: error.message }, { status: error.status ?? 400 });
41+
}
42+
43+
logger.error("Failed to reset idempotency key via API", {
44+
error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error),
45+
});
46+
47+
return json({ error: "Internal Server Error" }, { status: 500 });
48+
}
49+
50+
}
51+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { parse } from "@conform-to/zod";
2+
import { type ActionFunction, json } from "@remix-run/node";
3+
import { z } from "zod";
4+
import { prisma } from "~/db.server";
5+
import { jsonWithErrorMessage } from "~/models/message.server";
6+
import { logger } from "~/services/logger.server";
7+
import { requireUserId } from "~/services/session.server";
8+
import { ResetIdempotencyKeyService } from "~/v3/services/resetIdempotencyKey.server";
9+
import { v3RunParamsSchema } from "~/utils/pathBuilder";
10+
11+
export const resetIdempotencyKeySchema = z.object({
12+
taskIdentifier: z.string().min(1, "Task identifier is required"),
13+
});
14+
15+
export const action: ActionFunction = async ({ request, params }) => {
16+
const userId = await requireUserId(request);
17+
const { projectParam, organizationSlug, envParam, runParam } =
18+
v3RunParamsSchema.parse(params);
19+
20+
const formData = await request.formData();
21+
const submission = parse(formData, { schema: resetIdempotencyKeySchema });
22+
23+
if (!submission.value) {
24+
return json(submission);
25+
}
26+
27+
try {
28+
const { taskIdentifier } = submission.value;
29+
30+
const taskRun = await prisma.taskRun.findFirst({
31+
where: {
32+
friendlyId: runParam,
33+
project: {
34+
slug: projectParam,
35+
organization: {
36+
slug: organizationSlug,
37+
members: {
38+
some: {
39+
userId,
40+
},
41+
},
42+
},
43+
},
44+
runtimeEnvironment: {
45+
slug: envParam,
46+
},
47+
},
48+
select: {
49+
id: true,
50+
idempotencyKey: true,
51+
taskIdentifier: true,
52+
runtimeEnvironmentId: true,
53+
},
54+
});
55+
56+
if (!taskRun) {
57+
submission.error = { runParam: ["Run not found"] };
58+
return json(submission);
59+
}
60+
61+
if (!taskRun.idempotencyKey) {
62+
return jsonWithErrorMessage(
63+
submission,
64+
request,
65+
"This run does not have an idempotency key"
66+
);
67+
}
68+
69+
if (taskRun.taskIdentifier !== taskIdentifier) {
70+
submission.error = { taskIdentifier: ["Task identifier does not match this run"] };
71+
return json(submission);
72+
}
73+
74+
const environment = await prisma.runtimeEnvironment.findUnique({
75+
where: {
76+
id: taskRun.runtimeEnvironmentId,
77+
},
78+
include: {
79+
project: {
80+
include: {
81+
organization: true,
82+
},
83+
},
84+
},
85+
});
86+
87+
if (!environment) {
88+
return jsonWithErrorMessage(
89+
submission,
90+
request,
91+
"Environment not found"
92+
);
93+
}
94+
95+
const service = new ResetIdempotencyKeyService();
96+
97+
await service.call(taskRun.idempotencyKey, taskIdentifier, {
98+
...environment,
99+
organizationId: environment.project.organizationId,
100+
organization: environment.project.organization,
101+
});
102+
103+
return json({ success: true });
104+
} catch (error) {
105+
if (error instanceof Error) {
106+
logger.error("Failed to reset idempotency key", {
107+
error: {
108+
name: error.name,
109+
message: error.message,
110+
stack: error.stack,
111+
},
112+
});
113+
return jsonWithErrorMessage(
114+
submission,
115+
request,
116+
`Failed to reset idempotency key: ${error.message}`
117+
);
118+
} else {
119+
logger.error("Failed to reset idempotency key", { error });
120+
return jsonWithErrorMessage(
121+
submission,
122+
request,
123+
`Failed to reset idempotency key: ${JSON.stringify(error)}`
124+
);
125+
}
126+
}
127+
};

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ArrowPathIcon,
23
CheckIcon,
34
CloudArrowDownIcon,
45
EnvelopeIcon,
@@ -29,6 +30,7 @@ import { Header2, Header3 } from "~/components/primitives/Headers";
2930
import { Paragraph } from "~/components/primitives/Paragraph";
3031
import * as Property from "~/components/primitives/PropertyTable";
3132
import { Spinner } from "~/components/primitives/Spinner";
33+
import { toast } from "sonner";
3234
import {
3335
Table,
3436
TableBody,
@@ -40,6 +42,7 @@ import {
4042
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
4143
import { TextLink } from "~/components/primitives/TextLink";
4244
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
45+
import { ToastUI } from "~/components/primitives/Toast";
4346
import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/RunTimeline";
4447
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
4548
import { RunIcon } from "~/components/runs/v3/RunIcon";
@@ -69,6 +72,7 @@ import {
6972
v3BatchPath,
7073
v3DeploymentVersionPath,
7174
v3RunDownloadLogsPath,
75+
v3RunIdempotencyKeyResetPath,
7276
v3RunPath,
7377
v3RunRedirectPath,
7478
v3RunSpanPath,
@@ -81,6 +85,7 @@ import { CompleteWaitpointForm } from "../resources.orgs.$organizationSlug.proje
8185
import { requireUserId } from "~/services/session.server";
8286
import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types";
8387
import { RealtimeStreamViewer } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route";
88+
import { action as resetIdempotencyKeyAction } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.idempotencyKey.reset";
8489

8590
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8691
const userId = await requireUserId(request);
@@ -293,6 +298,28 @@ function RunBody({
293298
const isAdmin = useHasAdminAccess();
294299
const { value, replace } = useSearchParams();
295300
const tab = value("tab");
301+
const resetFetcher = useTypedFetcher<typeof resetIdempotencyKeyAction>();
302+
303+
// Handle toast messages from the reset action
304+
useEffect(() => {
305+
if (resetFetcher.data && resetFetcher.state === "idle") {
306+
// Check if the response indicates success
307+
if (resetFetcher.data && typeof resetFetcher.data === "object" && "success" in resetFetcher.data && resetFetcher.data.success === true) {
308+
toast.custom(
309+
(t) => (
310+
<ToastUI
311+
variant="success"
312+
message="Idempotency key reset successfully"
313+
t={t as string}
314+
/>
315+
),
316+
{
317+
duration: 5000,
318+
}
319+
);
320+
}
321+
}
322+
}, [resetFetcher.data, resetFetcher.state]);
296323

297324
return (
298325
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_3.25rem] overflow-hidden bg-background-bright">
@@ -543,17 +570,37 @@ function RunBody({
543570
<Property.Item>
544571
<Property.Label>Idempotency</Property.Label>
545572
<Property.Value>
546-
<div className="break-all">{run.idempotencyKey ? run.idempotencyKey : "–"}</div>
547-
{run.idempotencyKey && (
548-
<div>
549-
Expires:{" "}
550-
{run.idempotencyKeyExpiresAt ? (
551-
<DateTime date={run.idempotencyKeyExpiresAt} />
552-
) : (
553-
"–"
573+
<div className="flex items-start justify-between gap-2">
574+
<div className="flex-1">
575+
<div className="break-all">{run.idempotencyKey ? run.idempotencyKey : "–"}</div>
576+
{run.idempotencyKey && (
577+
<div>
578+
Expires:{" "}
579+
{run.idempotencyKeyExpiresAt ? (
580+
<DateTime date={run.idempotencyKeyExpiresAt} />
581+
) : (
582+
"–"
583+
)}
584+
</div>
554585
)}
555586
</div>
556-
)}
587+
{run.idempotencyKey && (
588+
<resetFetcher.Form
589+
method="post"
590+
action={v3RunIdempotencyKeyResetPath(organization, project, environment, { friendlyId: runParam })}
591+
>
592+
<input type="hidden" name="taskIdentifier" value={run.taskIdentifier} />
593+
<Button
594+
type="submit"
595+
variant="minimal/small"
596+
LeadingIcon={ArrowPathIcon}
597+
disabled={resetFetcher.state === "submitting"}
598+
>
599+
{resetFetcher.state === "submitting" ? "Resetting..." : "Reset"}
600+
</Button>
601+
</resetFetcher.Form>
602+
)}
603+
</div>
557604
</Property.Value>
558605
</Property.Item>
559606
<Property.Item>

0 commit comments

Comments
 (0)