Skip to content

Commit db6f7d4

Browse files
committed
dedupe loops ui helpers and fallbacks
1 parent 70a8818 commit db6f7d4

9 files changed

Lines changed: 140 additions & 155 deletions

File tree

packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { navigateToNewLoop } from "@posthog/ui/router/navigationBridge";
66
import { Flex, Heading, Text } from "@radix-ui/themes";
77
import { useMemo } from "react";
88
import { LoopBuilderComposer } from "../../loops/components/LoopBuilderComposer";
9+
import {
10+
LoopsEmptyNotice,
11+
LoopsSkeleton,
12+
} from "../../loops/components/LoopFallbacks";
913
import { LoopRow } from "../../loops/components/LoopRow";
1014
import { LoopsEmptyState } from "../../loops/components/LoopsEmptyState";
1115
import { useLoops } from "../../loops/hooks/useLoops";
@@ -108,7 +112,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
108112
{isLoading ? (
109113
<LoopsSkeleton />
110114
) : isError ? (
111-
<EmptyNotice
115+
<LoopsEmptyNotice
112116
title="Couldn't load loops"
113117
hint="The loops API returned an error. Try again in a moment."
114118
/>
@@ -145,34 +149,3 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
145149
</Flex>
146150
);
147151
}
148-
149-
function EmptyNotice({ title, hint }: { title: string; hint: string }) {
150-
return (
151-
<Flex
152-
align="center"
153-
justify="center"
154-
direction="column"
155-
gap="1"
156-
py="7"
157-
className="rounded border border-gray-6 border-dashed"
158-
>
159-
<Text className="font-medium text-sm">{title}</Text>
160-
<Text color="gray" className="max-w-[420px] text-center text-[13px]">
161-
{hint}
162-
</Text>
163-
</Flex>
164-
);
165-
}
166-
167-
function LoopsSkeleton() {
168-
return (
169-
<Flex direction="column" gap="2">
170-
{[0, 1, 2].map((i) => (
171-
<div
172-
key={i}
173-
className="h-[68px] animate-pulse rounded-(--radius-2) border border-border bg-(--gray-2)"
174-
/>
175-
))}
176-
</Flex>
177-
);
178-
}

packages/ui/src/features/loops/components/EditLoopView.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Flex, Text } from "@radix-ui/themes";
21
import { useLoop } from "../hooks/useLoop";
2+
import { LoopLoadError } from "./LoopFallbacks";
33
import { LoopForm } from "./LoopForm";
44

55
export function EditLoopView({ loopId }: { loopId: string }) {
@@ -14,21 +14,7 @@ export function EditLoopView({ loopId }: { loopId: string }) {
1414
}
1515

1616
if (isError || !loop) {
17-
return (
18-
<Flex
19-
direction="column"
20-
align="center"
21-
gap="1"
22-
className="mx-auto mt-16 max-w-md rounded-(--radius-2) border border-(--gray-5) border-dashed px-6 py-10 text-center"
23-
>
24-
<Text className="font-medium text-[13px] text-gray-12">
25-
Couldn't load this loop
26-
</Text>
27-
<Text className="max-w-md text-[12px] text-gray-11 leading-snug">
28-
It may have been deleted, or the loops API returned an error.
29-
</Text>
30-
</Flex>
31-
);
17+
return <LoopLoadError />;
3218
}
3319

3420
return <LoopForm loop={loop} />;

packages/ui/src/features/loops/components/LoopDetailView.tsx

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,15 @@ import {
1717
useRunLoop,
1818
useUpdateLoop,
1919
} from "../hooks/useLoopMutations";
20-
import { useLoopRuns } from "../hooks/useLoopRuns";
20+
import { RECENT_RUNS_LIMIT, useLoopRuns } from "../hooks/useLoopRuns";
21+
import {
22+
describeTrigger,
23+
loopStatusColor,
24+
loopStatusLabel,
25+
} from "../loopDisplay";
26+
import { LoopLoadError } from "./LoopFallbacks";
2127
import { LoopRunRow } from "./LoopRunRow";
2228

23-
function statusColor(loop: LoopSchemas.Loop): "gray" | "green" | "red" {
24-
if (!loop.enabled) return "gray";
25-
if (loop.last_run_status === "failed") return "red";
26-
return "green";
27-
}
28-
29-
function statusLabel(loop: LoopSchemas.Loop): string {
30-
if (!loop.enabled) return "Paused";
31-
if (loop.last_run_status === "failed") return "Failing";
32-
return "Active";
33-
}
34-
35-
function describeTrigger(trigger: LoopSchemas.LoopTrigger): string {
36-
if (trigger.type === "schedule") {
37-
const config = trigger.config as LoopSchemas.LoopScheduleTriggerConfig;
38-
if (config.run_at)
39-
return `One-time · ${new Date(config.run_at).toLocaleString()}`;
40-
return `Schedule · ${config.cron_expression ?? "?"} (${config.timezone ?? "UTC"})`;
41-
}
42-
if (trigger.type === "github") {
43-
const config = trigger.config as LoopSchemas.LoopGithubTriggerConfig;
44-
return `GitHub · ${config.repository || "?"} · ${config.events.join(", ") || "no events"}`;
45-
}
46-
return "API · authenticated POST";
47-
}
48-
4929
export function LoopDetailView({ loopId }: { loopId: string }) {
5030
const { data: loop, isLoading, isError } = useLoop(loopId);
5131
const updateLoop = useUpdateLoop(loopId);
@@ -114,21 +94,7 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
11494
}
11595

11696
if (isError || !loop) {
117-
return (
118-
<Flex
119-
direction="column"
120-
align="center"
121-
gap="1"
122-
className="mx-auto mt-16 max-w-md rounded-(--radius-2) border border-(--gray-5) border-dashed px-6 py-10 text-center"
123-
>
124-
<Text className="font-medium text-[13px] text-gray-12">
125-
Couldn't load this loop
126-
</Text>
127-
<Text className="max-w-md text-[12px] text-gray-11 leading-snug">
128-
It may have been deleted, or the loops API returned an error.
129-
</Text>
130-
</Flex>
131-
);
97+
return <LoopLoadError />;
13298
}
13399

134100
return (
@@ -149,7 +115,9 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
149115
<Text className="font-bold text-[22px] text-gray-12 leading-tight tracking-tight">
150116
{loop.name}
151117
</Text>
152-
<Badge color={statusColor(loop)}>{statusLabel(loop)}</Badge>
118+
<Badge color={loopStatusColor(loop)}>
119+
{loopStatusLabel(loop)}
120+
</Badge>
153121
<Badge color="gray">{loop.visibility}</Badge>
154122
</Flex>
155123
<Flex align="center" gap="2">
@@ -202,7 +170,9 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
202170
<Text className="font-medium text-[13px] text-gray-12">
203171
Run history
204172
</Text>
205-
<Text className="text-[11px] text-gray-10">10 most recent</Text>
173+
<Text className="text-[11px] text-gray-10">
174+
{RECENT_RUNS_LIMIT} most recent
175+
</Text>
206176
</Flex>
207177
{runsQuery.isLoading ? (
208178
<div className="h-16 animate-pulse rounded-(--radius-2) border border-border bg-(--gray-2)" />
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Flex, Text } from "@radix-ui/themes";
2+
3+
export function LoopLoadError() {
4+
return (
5+
<Flex
6+
direction="column"
7+
align="center"
8+
gap="1"
9+
className="mx-auto mt-16 max-w-md rounded-(--radius-2) border border-(--gray-5) border-dashed px-6 py-10 text-center"
10+
>
11+
<Text className="font-medium text-[13px] text-gray-12">
12+
Couldn't load this loop
13+
</Text>
14+
<Text className="max-w-md text-[12px] text-gray-11 leading-snug">
15+
It may have been deleted, or the loops API returned an error.
16+
</Text>
17+
</Flex>
18+
);
19+
}
20+
21+
export function LoopsEmptyNotice({
22+
title,
23+
hint,
24+
}: {
25+
title: string;
26+
hint: string;
27+
}) {
28+
return (
29+
<Flex
30+
align="center"
31+
justify="center"
32+
direction="column"
33+
gap="1"
34+
py="6"
35+
className="rounded border border-gray-6 border-dashed"
36+
>
37+
<Text className="font-medium text-sm">{title}</Text>
38+
<Text color="gray" className="max-w-[420px] text-center text-[13px]">
39+
{hint}
40+
</Text>
41+
</Flex>
42+
);
43+
}
44+
45+
export function LoopsSkeleton() {
46+
return (
47+
<Flex direction="column" gap="2">
48+
{[0, 1, 2].map((i) => (
49+
<div
50+
key={i}
51+
className="h-[58px] animate-pulse rounded-(--radius-2) border border-border bg-(--gray-2)"
52+
/>
53+
))}
54+
</Flex>
55+
);
56+
}

packages/ui/src/features/loops/components/LoopForm.tsx

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Box, Flex, Text, TextArea, TextField } from "@radix-ui/themes";
1717
import { type ReactNode, useEffect, useState } from "react";
1818
import { useAuthStateValue } from "../../auth/store";
1919
import { useCreateLoop, useUpdateLoop } from "../hooks/useLoopMutations";
20+
import { summarizeTrigger } from "../loopDisplay";
2021
import { useLoopDraftStore } from "../loopDraftStore";
2122
import {
2223
emptyLoopFormValues,
@@ -142,7 +143,12 @@ export function LoopForm({ loop }: LoopFormProps) {
142143
return;
143144
}
144145
toast.error(isEdit ? "Failed to save loop" : "Failed to create loop", {
145-
description: error instanceof Error ? error.message : undefined,
146+
description:
147+
error instanceof LoopsApiError
148+
? (error.detail ?? error.message)
149+
: error instanceof Error
150+
? error.message
151+
: undefined,
146152
});
147153
}
148154
};
@@ -536,7 +542,7 @@ function ReviewList({ values }: { values: LoopFormValues }) {
536542
value={
537543
values.triggers.length === 0
538544
? "Manual only"
539-
: values.triggers.map(describeTrigger).join(", ")
545+
: values.triggers.map(summarizeTrigger).join(", ")
540546
}
541547
/>
542548
<ReviewRow
@@ -584,17 +590,3 @@ function describeContext(target: LoopContextTargetDraft | null): string {
584590
? `#${target.name} (${outputs.join(", ")})`
585591
: `#${target.name}`;
586592
}
587-
588-
function describeTrigger(trigger: LoopFormValues["triggers"][number]): string {
589-
if (trigger.type === "schedule") {
590-
const config = trigger.config as LoopSchemas.LoopScheduleTriggerConfig;
591-
if (config.run_at) return "Once";
592-
return `Schedule (${config.cron_expression ?? "cron"})`;
593-
}
594-
if (trigger.type === "github") {
595-
const config = trigger.config as LoopSchemas.LoopGithubTriggerConfig;
596-
const repo = config.repository || "a repo";
597-
return `GitHub (${repo})`;
598-
}
599-
return "API";
600-
}

packages/ui/src/features/loops/components/LoopRow.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ import type { LoopSchemas } from "@posthog/api-client/loops";
33
import { Badge } from "@posthog/ui/primitives/Badge";
44
import { Flex, Text } from "@radix-ui/themes";
55
import { Link } from "@tanstack/react-router";
6-
7-
function statusColor(loop: LoopSchemas.Loop): "gray" | "green" | "red" {
8-
if (!loop.enabled) return "gray";
9-
if (loop.last_run_status === "failed") return "red";
10-
return "green";
11-
}
12-
13-
function statusLabel(loop: LoopSchemas.Loop): string {
14-
if (!loop.enabled) return "Paused";
15-
if (loop.last_run_status === "failed") return "Failing";
16-
return "Active";
17-
}
6+
import { loopStatusColor, loopStatusLabel } from "../loopDisplay";
187

198
export function LoopRow({ loop }: { loop: LoopSchemas.Loop }) {
209
return (
@@ -30,7 +19,7 @@ export function LoopRow({ loop }: { loop: LoopSchemas.Loop }) {
3019
<Text className="truncate font-medium text-[13px] text-gray-12">
3120
{loop.name}
3221
</Text>
33-
<Badge color={statusColor(loop)}>{statusLabel(loop)}</Badge>
22+
<Badge color={loopStatusColor(loop)}>{loopStatusLabel(loop)}</Badge>
3423
</Flex>
3524
<Text className="truncate text-[12px] text-gray-11 leading-snug">
3625
{loop.description.trim()

packages/ui/src/features/loops/components/LoopsListView.tsx

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useLoops } from "../hooks/useLoops";
88
import { useLoopDraftStore } from "../loopDraftStore";
99
import type { LoopTemplate } from "../loopTemplates";
1010
import { LoopBuilderComposer } from "./LoopBuilderComposer";
11+
import { LoopsEmptyNotice, LoopsSkeleton } from "./LoopFallbacks";
1112
import { LoopRow } from "./LoopRow";
1213
import { LoopsEmptyState } from "./LoopsEmptyState";
1314
import { LoopTemplatesSection } from "./LoopTemplatesSection";
@@ -84,7 +85,7 @@ export function LoopsListView() {
8485
{isLoading ? (
8586
<LoopsSkeleton />
8687
) : isError ? (
87-
<EmptyNotice
88+
<LoopsEmptyNotice
8889
title="Couldn't load loops."
8990
hint={
9091
error instanceof Error
@@ -123,34 +124,3 @@ export function LoopsListView() {
123124
</Flex>
124125
);
125126
}
126-
127-
function EmptyNotice({ title, hint }: { title: string; hint: string }) {
128-
return (
129-
<Flex
130-
align="center"
131-
justify="center"
132-
direction="column"
133-
gap="1"
134-
py="6"
135-
className="rounded border border-gray-6 border-dashed"
136-
>
137-
<Text className="font-medium text-sm">{title}</Text>
138-
<Text color="gray" className="text-[13px]">
139-
{hint}
140-
</Text>
141-
</Flex>
142-
);
143-
}
144-
145-
function LoopsSkeleton() {
146-
return (
147-
<Flex direction="column" gap="2">
148-
{[0, 1, 2].map((i) => (
149-
<div
150-
key={i}
151-
className="h-[58px] animate-pulse rounded-(--radius-2) border border-border bg-(--gray-2)"
152-
/>
153-
))}
154-
</Flex>
155-
);
156-
}

packages/ui/src/features/loops/hooks/useLoopRuns.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { useQuery } from "@tanstack/react-query";
44
import { loopsKeys } from "./loopsKeys";
55
import { useLoopsClient } from "./useLoopsClient";
66

7-
const RECENT_RUNS_LIMIT = 10;
7+
export const RECENT_RUNS_LIMIT = 10;
88

9-
/** The 10 most recent runs for a loop, polled so the detail view stays live. */
9+
/** The most recent runs for a loop, polled so the detail view stays live. */
1010
export function useLoopRuns(loopId: string | undefined) {
1111
const loopsClient = useLoopsClient();
1212

0 commit comments

Comments
 (0)