Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 137 additions & 56 deletions packages/ui/src/features/loops/components/LoopDetailView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { ArrowLeftIcon, RepeatIcon } from "@phosphor-icons/react";
import type { LoopSchemas } from "@posthog/api-client/loops";
import { Switch } from "@posthog/quill";
import {
AlertDialog,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Badge,
Button,
Switch,
Textarea,
} from "@posthog/quill";
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
import { Badge } from "@posthog/ui/primitives/Badge";
import { Button } from "@posthog/ui/primitives/Button";
import { toast } from "@posthog/ui/primitives/toast";
import {
navigateToEditLoop,
navigateToLoops,
} from "@posthog/ui/router/navigationBridge";
import { AlertDialog, Flex, Text } from "@radix-ui/themes";
import { useState } from "react";
import { Flex, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
import { useLoop } from "../hooks/useLoop";
import {
useDeleteLoop,
Expand Down Expand Up @@ -101,24 +111,25 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
<div className="mx-auto w-full max-w-5xl px-8 py-6">
<Flex direction="column" gap="5">
<Flex direction="column" gap="3">
<button
type="button"
<Button
variant="link-muted"
size="xs"
onClick={navigateToLoops}
className="flex w-fit items-center gap-1.5 border-none bg-transparent p-0 text-[12px] text-gray-11 no-underline hover:text-gray-12"
className="w-fit px-0"
>
<ArrowLeftIcon size={13} />
Loops
</button>
</Button>

<Flex align="center" justify="between" gap="3" wrap="wrap">
<Flex align="center" gap="2" wrap="wrap">
<Text className="font-bold text-[22px] text-gray-12 leading-tight tracking-tight">
{loop.name}
</Text>
<Badge color={loopStatusColor(loop)}>
<Badge variant={loopStatusBadgeVariant(loop)}>
{loopStatusLabel(loop)}
</Badge>
<Badge color="gray">{loop.visibility}</Badge>
<Badge>{loop.visibility}</Badge>
</Flex>
<Flex align="center" gap="2">
<Switch
Expand All @@ -128,27 +139,24 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
onCheckedChange={handleToggleEnabled}
/>
<Button
variant="soft"
color="gray"
size="1"
variant="outline"
size="xs"
loading={runLoop.isPending}
disabled={runLoop.isPending}
onClick={handleRunNow}
>
Run now
</Button>
<Button
variant="soft"
color="gray"
size="1"
variant="outline"
size="xs"
onClick={() => navigateToEditLoop(loop.id)}
>
Edit
</Button>
<Button
variant="soft"
color="red"
size="1"
variant="destructive"
size="xs"
onClick={() => setDeleteOpen(true)}
>
Delete
Expand All @@ -165,6 +173,8 @@ export function LoopDetailView({ loopId }: { loopId: string }) {

<ConfigSummarySection loop={loop} />

<InstructionsSection loop={loop} />

<Flex direction="column" gap="2">
<Flex align="center" gap="2">
<Text className="font-medium text-[13px] text-gray-12">
Expand Down Expand Up @@ -201,41 +211,51 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
</Flex>
</Flex>

<AlertDialog.Root open={deleteOpen} onOpenChange={setDeleteOpen}>
<AlertDialog.Content maxWidth="420px" size="1">
<AlertDialog.Title className="text-sm">Delete loop</AlertDialog.Title>
<AlertDialog.Description className="text-[13px]">
<Text color="gray" className="text-[13px]">
Permanently delete{" "}
<Text className="font-medium text-[13px]">{loop.name}</Text>? This
stops every trigger and cannot be undone.
</Text>
</AlertDialog.Description>
<Flex justify="end" gap="3" mt="3">
<AlertDialog.Cancel>
<Button variant="soft" color="gray" size="1">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button
variant="solid"
color="red"
size="1"
loading={deleteLoop.isPending}
disabled={deleteLoop.isPending}
onClick={handleDelete}
>
Delete
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<AlertDialogContent className="max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Delete loop</AlertDialogTitle>
<AlertDialogDescription>
<Text color="gray" className="text-[13px]">
Permanently delete{" "}
<Text className="font-medium text-[13px]">{loop.name}</Text>?
This stops every trigger and cannot be undone.
</Text>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogClose
render={
<Button variant="outline" size="sm">
Cancel
</Button>
}
/>
<Button
variant="destructive"
size="sm"
loading={deleteLoop.isPending}
disabled={deleteLoop.isPending}
onClick={handleDelete}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

function loopStatusBadgeVariant(
loop: LoopSchemas.Loop,
): "default" | "destructive" | "success" {
const color = loopStatusColor(loop);
if (color === "green") return "success";
if (color === "red") return "destructive";
return "default";
}

function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
return (
<Flex direction="column" gap="3">
Expand Down Expand Up @@ -278,13 +298,74 @@ function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
</Flex>
)}
</SummaryRow>
</Flex>
</Flex>
);
}

<SummaryRow label="Instructions">
<pre className="max-h-[200px] overflow-auto whitespace-pre-wrap text-[12px] text-gray-12 [font-family:var(--font-mono)]">
{loop.instructions}
</pre>
</SummaryRow>
function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
const updateLoop = useUpdateLoop(loop.id);
const [draft, setDraft] = useState<string | null>(null);
// Escape reverts and blurs; skip the resulting onBlur save.
const skipCommit = useRef(false);

const commit = (value: string) => {
if (skipCommit.current) {
skipCommit.current = false;
return;
}
const trimmed = value.trim();
if (!trimmed) {
setDraft(null);
return;
}
if (updateLoop.isPending) return;
if (trimmed === loop.instructions.trim()) {
setDraft(null);
return;
}
updateLoop.mutate(
{ instructions: trimmed },
Comment thread
MattPua marked this conversation as resolved.
{
onSuccess: () => {
setDraft(null);
toast.success("Instructions updated");
},
onError: (error) => {
setDraft(null);
toast.error("Failed to update instructions", {
description: error.message,
});
},
},
);
};

return (
<Flex direction="column" gap="3">
<Flex align="center" gap="2">
<Text className="font-medium text-[13px] text-gray-12">
Instructions
</Text>
{updateLoop.isPending ? (
<Text className="text-[11px] text-gray-10">Saving…</Text>
) : null}
</Flex>
<Textarea
value={draft ?? loop.instructions}
disabled={updateLoop.isPending}
aria-label="Loop instructions"
className="min-h-[200px] bg-(--color-panel-solid) text-[12.5px] leading-relaxed"
onChange={(e) => setDraft(e.currentTarget.value)}
onBlur={(e) => commit(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Escape") {
skipCommit.current = true;
setDraft(null);
e.currentTarget.blur();
}
}}
/>
</Flex>
);
}
Expand Down
Loading