feat(ui): move routine triggers/runs/activity tabs into properties panel
Reuses the global PropertiesPanel (open/close persisted to localStorage) for routine detail, mirroring the IssueDetail pattern. Adds a header toggle button to reopen the panel, and keeps mobile parity by rendering the same tabs inline below the md breakpoint. Also restructures the trigger card and runs/activity rows into vertically stacked layouts so they fit the 320px panel without overflow, and replaces the last-result badge with a wrapping inline chip so long error strings no longer fill the card width.
This commit is contained in:
parent
2ba5e6ad4f
commit
145a86b512
|
|
@ -49,90 +49,90 @@ export function TriggerListCard({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg border border-border p-4 transition-colors ${trigger.enabled ? "bg-card" : "bg-muted/40"}`}
|
className={`rounded-lg border border-border p-3 transition-colors ${trigger.enabled ? "bg-card" : "bg-muted/40"}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
<span className={`text-sm font-medium truncate flex-1 min-w-0 ${trigger.enabled ? "" : "text-muted-foreground"}`}>
|
||||||
|
{trigger.label || (isSchedule ? "Schedule" : isWebhook ? "Webhook" : "Trigger")}
|
||||||
|
</span>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={trigger.enabled}
|
||||||
|
onCheckedChange={onToggleEnabled}
|
||||||
|
disabled={togglePending}
|
||||||
|
aria-label={trigger.enabled ? "Disable trigger" : "Enable trigger"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="flex items-center gap-1.5 flex-wrap mt-2">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<Badge variant="outline" className="text-[11px]">
|
||||||
<span className={`text-sm font-medium ${trigger.enabled ? "" : "text-muted-foreground"}`}>
|
{trigger.kind}
|
||||||
{trigger.label || (isSchedule ? "Schedule" : isWebhook ? "Webhook" : "Trigger")}
|
</Badge>
|
||||||
</span>
|
{!trigger.enabled && (
|
||||||
<Badge variant="outline" className="text-[11px]">
|
<Badge variant="secondary" className="text-[11px] text-muted-foreground">
|
||||||
{trigger.kind}
|
paused
|
||||||
</Badge>
|
</Badge>
|
||||||
{!trigger.enabled && (
|
)}
|
||||||
<Badge variant="secondary" className="text-[11px] text-muted-foreground">
|
</div>
|
||||||
paused
|
|
||||||
</Badge>
|
<div className="mt-2 text-sm break-words">{summary}</div>
|
||||||
|
{isSchedule && trigger.cronExpression && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-1 font-mono break-all">
|
||||||
|
{trigger.cronExpression}
|
||||||
|
{trigger.timezone ? ` · ${trigger.timezone}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<dl className="mt-3 space-y-2 text-xs">
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<dt className="text-muted-foreground">Next run</dt>
|
||||||
|
<dd className="break-words">{nextRun}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<dt className="text-muted-foreground">Last fired</dt>
|
||||||
|
<dd className="break-words">{lastFired}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<dt className="text-muted-foreground">Last result</dt>
|
||||||
|
<dd className="min-w-0">
|
||||||
|
{trigger.lastResult ? (
|
||||||
|
<span
|
||||||
|
className={`inline-block rounded px-1.5 py-0.5 text-[11px] break-words ${
|
||||||
|
resultIsError
|
||||||
|
? "bg-destructive/15 text-destructive"
|
||||||
|
: "bg-secondary text-secondary-foreground"
|
||||||
|
}`}
|
||||||
|
title={trigger.lastResult}
|
||||||
|
>
|
||||||
|
{trigger.lastResult}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</dd>
|
||||||
<div className="text-sm mt-1.5">{summary}</div>
|
|
||||||
{isSchedule && trigger.cronExpression && (
|
|
||||||
<div className="text-xs text-muted-foreground mt-1 font-mono">
|
|
||||||
{trigger.cronExpression}
|
|
||||||
{trigger.timezone ? ` · ${trigger.timezone}` : ""}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-3 mt-4 text-xs">
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground mb-0.5">Next run</div>
|
|
||||||
<div>{nextRun}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground mb-0.5">Last fired</div>
|
|
||||||
<div>{lastFired}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground mb-0.5">Last result</div>
|
|
||||||
<div>
|
|
||||||
{trigger.lastResult ? (
|
|
||||||
<Badge
|
|
||||||
variant={resultIsError ? "destructive" : "secondary"}
|
|
||||||
className="text-[11px] max-w-full truncate"
|
|
||||||
title={trigger.lastResult}
|
|
||||||
>
|
|
||||||
{trigger.lastResult}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">—</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-3 shrink-0">
|
<div className="mt-3 flex items-center justify-end gap-1 border-t border-border pt-2">
|
||||||
<ToggleSwitch
|
{isWebhook && onRotateSecret && (
|
||||||
checked={trigger.enabled}
|
<Button variant="ghost" size="xs" onClick={onRotateSecret} title="Rotate secret">
|
||||||
onCheckedChange={onToggleEnabled}
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
disabled={togglePending}
|
</Button>
|
||||||
aria-label={trigger.enabled ? "Disable trigger" : "Enable trigger"}
|
)}
|
||||||
/>
|
<Button variant="ghost" size="xs" onClick={onEdit} title="Edit">
|
||||||
<div className="flex gap-1">
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
{isWebhook && onRotateSecret && (
|
</Button>
|
||||||
<Button variant="ghost" size="xs" onClick={onRotateSecret} title="Rotate secret">
|
<Button
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
variant="ghost"
|
||||||
</Button>
|
size="xs"
|
||||||
)}
|
onClick={onDelete}
|
||||||
<Button variant="ghost" size="xs" onClick={onEdit} title="Edit">
|
title="Delete"
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
className="text-muted-foreground hover:text-destructive"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="xs"
|
|
||||||
onClick={onDelete}
|
|
||||||
title="Delete"
|
|
||||||
className="text-muted-foreground hover:text-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Plus,
|
Plus,
|
||||||
Repeat,
|
Repeat,
|
||||||
Save,
|
Save,
|
||||||
|
SlidersHorizontal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { routinesApi, type RoutineTriggerResponse, type RotateRoutineTriggerResponse } from "../api/routines";
|
import { routinesApi, type RoutineTriggerResponse, type RotateRoutineTriggerResponse } from "../api/routines";
|
||||||
import { TriggerListCard } from "../components/TriggerListCard";
|
import { TriggerListCard } from "../components/TriggerListCard";
|
||||||
|
|
@ -22,7 +23,9 @@ import { agentsApi } from "../api/agents";
|
||||||
import { projectsApi } from "../api/projects";
|
import { projectsApi } from "../api/projects";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||||
|
import { usePanel } from "../context/PanelContext";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { timeAgo } from "../lib/timeAgo";
|
import { timeAgo } from "../lib/timeAgo";
|
||||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||||
|
|
@ -136,6 +139,7 @@ export function RoutineDetail() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pushToast } = useToast();
|
const { pushToast } = useToast();
|
||||||
|
const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel();
|
||||||
const hydratedRoutineIdRef = useRef<string | null>(null);
|
const hydratedRoutineIdRef = useRef<string | null>(null);
|
||||||
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||||
|
|
@ -519,6 +523,167 @@ export function RoutineDetail() {
|
||||||
const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null;
|
const currentAssignee = editDraft.assigneeAgentId ? agentById.get(editDraft.assigneeAgentId) ?? null : null;
|
||||||
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
|
const currentProject = editDraft.projectId ? projectById.get(editDraft.projectId) ?? null : null;
|
||||||
|
|
||||||
|
const renderActivityTabs = () => {
|
||||||
|
if (!routine) return null;
|
||||||
|
return (
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-3">
|
||||||
|
<TabsList variant="line" className="w-full justify-start gap-1">
|
||||||
|
<TabsTrigger value="triggers" className="gap-1.5">
|
||||||
|
<Clock3 className="h-3.5 w-3.5" />
|
||||||
|
Triggers
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="runs" className="gap-1.5">
|
||||||
|
<Play className="h-3.5 w-3.5" />
|
||||||
|
Runs
|
||||||
|
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="activity" className="gap-1.5">
|
||||||
|
<ActivityIcon className="h-3.5 w-3.5" />
|
||||||
|
Activity
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="triggers" className="space-y-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingTrigger(null);
|
||||||
|
setTriggerDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Add trigger
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{routine.triggers.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-dashed border-border bg-muted/30 p-8 text-center">
|
||||||
|
<p className="text-sm font-medium">No triggers yet</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 mb-4">
|
||||||
|
Triggers fire this routine on a schedule or via webhook.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingTrigger(null);
|
||||||
|
setTriggerDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Add your first trigger
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{routine.triggers.map((trigger) => (
|
||||||
|
<TriggerListCard
|
||||||
|
key={trigger.id}
|
||||||
|
trigger={trigger}
|
||||||
|
onEdit={() => {
|
||||||
|
setEditingTrigger(trigger);
|
||||||
|
setTriggerDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onDelete={() => setTriggerPendingDelete(trigger)}
|
||||||
|
onToggleEnabled={(enabled) => {
|
||||||
|
setTogglingTriggerId(trigger.id);
|
||||||
|
updateTrigger.mutate({ id: trigger.id, patch: { enabled } });
|
||||||
|
}}
|
||||||
|
onRotateSecret={
|
||||||
|
trigger.kind === "webhook"
|
||||||
|
? () => rotateTrigger.mutate(trigger.id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
togglePending={togglingTriggerId === trigger.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="runs" className="space-y-4">
|
||||||
|
{hasLiveRun && activeIssueId && routine && (
|
||||||
|
<LiveRunWidget issueId={activeIssueId} companyId={routine.companyId} />
|
||||||
|
)}
|
||||||
|
{(routineRuns ?? []).length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">No runs yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="border border-border rounded-lg divide-y divide-border">
|
||||||
|
{(routineRuns ?? []).map((run) => (
|
||||||
|
<div key={run.id} className="flex flex-col gap-1.5 px-3 py-2 text-sm min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<Badge variant="outline" className="text-[11px]">{run.source}</Badge>
|
||||||
|
<Badge variant={run.status === "failed" ? "destructive" : "secondary"} className="text-[11px]">
|
||||||
|
{run.status.replaceAll("_", " ")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{(run.trigger || run.linkedIssue) && (
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap text-xs min-w-0">
|
||||||
|
{run.trigger && (
|
||||||
|
<span className="text-muted-foreground truncate">{run.trigger.label ?? run.trigger.kind}</span>
|
||||||
|
)}
|
||||||
|
{run.linkedIssue && (
|
||||||
|
<Link to={`/issues/${run.linkedIssue.identifier ?? run.linkedIssue.id}`} className="text-muted-foreground hover:underline truncate">
|
||||||
|
{run.linkedIssue.identifier ?? run.linkedIssue.id.slice(0, 8)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-[11px] text-muted-foreground">{timeAgo(run.triggeredAt)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="activity">
|
||||||
|
{(activity ?? []).length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">No activity yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="border border-border rounded-lg divide-y divide-border">
|
||||||
|
{(activity ?? []).map((event) => (
|
||||||
|
<div key={event.id} className="flex flex-col gap-1 px-3 py-2 text-xs min-w-0">
|
||||||
|
<span className="font-medium text-foreground/90">{event.action.replaceAll(".", " ")}</span>
|
||||||
|
{event.details && Object.keys(event.details).length > 0 && (
|
||||||
|
<div className="text-muted-foreground break-words">
|
||||||
|
{Object.entries(event.details).slice(0, 3).map(([key, value], i) => (
|
||||||
|
<span key={key}>
|
||||||
|
{i > 0 && <span className="mx-1 text-border">·</span>}
|
||||||
|
<span className="text-muted-foreground/70">{key.replaceAll("_", " ")}:</span>{" "}
|
||||||
|
{formatActivityDetailValue(value)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-muted-foreground/60">{timeAgo(event.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!routine) {
|
||||||
|
closePanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openPanel(renderActivityTabs());
|
||||||
|
return () => closePanel();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
routine,
|
||||||
|
routineRuns,
|
||||||
|
activity,
|
||||||
|
activeTab,
|
||||||
|
hasLiveRun,
|
||||||
|
activeIssueId,
|
||||||
|
togglingTriggerId,
|
||||||
|
openPanel,
|
||||||
|
closePanel,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!selectedCompanyId) {
|
if (!selectedCompanyId) {
|
||||||
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
||||||
}
|
}
|
||||||
|
|
@ -612,6 +777,18 @@ export function RoutineDetail() {
|
||||||
<span className={`min-w-[3.75rem] text-sm font-medium ${automationLabelClassName}`}>
|
<span className={`min-w-[3.75rem] text-sm font-medium ${automationLabelClassName}`}>
|
||||||
{automationLabel}
|
{automationLabel}
|
||||||
</span>
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
className={cn(
|
||||||
|
"hidden md:inline-flex shrink-0 transition-opacity duration-200",
|
||||||
|
panelVisible ? "opacity-0 pointer-events-none w-0 overflow-hidden" : "opacity-100",
|
||||||
|
)}
|
||||||
|
onClick={() => setPanelVisible(true)}
|
||||||
|
title="Show triggers, runs and activity"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -820,149 +997,12 @@ export function RoutineDetail() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator className="md:hidden" />
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs (mobile only — desktop renders in the right properties panel) */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-3">
|
<div className="md:hidden">
|
||||||
<TabsList variant="line" className="w-full justify-start gap-1">
|
{renderActivityTabs()}
|
||||||
<TabsTrigger value="triggers" className="gap-1.5">
|
</div>
|
||||||
<Clock3 className="h-3.5 w-3.5" />
|
|
||||||
Triggers
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="runs" className="gap-1.5">
|
|
||||||
<Play className="h-3.5 w-3.5" />
|
|
||||||
Runs
|
|
||||||
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="activity" className="gap-1.5">
|
|
||||||
<ActivityIcon className="h-3.5 w-3.5" />
|
|
||||||
Activity
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="triggers" className="space-y-4">
|
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-medium">Triggers</h2>
|
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
|
||||||
Schedules and webhooks that fire this routine.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingTrigger(null);
|
|
||||||
setTriggerDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
Add trigger
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{routine.triggers.length === 0 ? (
|
|
||||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 p-8 text-center">
|
|
||||||
<p className="text-sm font-medium">No triggers yet</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1 mb-4">
|
|
||||||
Triggers fire this routine on a schedule or via webhook.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingTrigger(null);
|
|
||||||
setTriggerDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
Add your first trigger
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{routine.triggers.map((trigger) => (
|
|
||||||
<TriggerListCard
|
|
||||||
key={trigger.id}
|
|
||||||
trigger={trigger}
|
|
||||||
onEdit={() => {
|
|
||||||
setEditingTrigger(trigger);
|
|
||||||
setTriggerDialogOpen(true);
|
|
||||||
}}
|
|
||||||
onDelete={() => setTriggerPendingDelete(trigger)}
|
|
||||||
onToggleEnabled={(enabled) => {
|
|
||||||
setTogglingTriggerId(trigger.id);
|
|
||||||
updateTrigger.mutate({ id: trigger.id, patch: { enabled } });
|
|
||||||
}}
|
|
||||||
onRotateSecret={
|
|
||||||
trigger.kind === "webhook"
|
|
||||||
? () => rotateTrigger.mutate(trigger.id)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
togglePending={togglingTriggerId === trigger.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="runs" className="space-y-4">
|
|
||||||
{hasLiveRun && activeIssueId && routine && (
|
|
||||||
<LiveRunWidget issueId={activeIssueId} companyId={routine.companyId} />
|
|
||||||
)}
|
|
||||||
{(routineRuns ?? []).length === 0 ? (
|
|
||||||
<p className="text-xs text-muted-foreground">No runs yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="border border-border rounded-lg divide-y divide-border">
|
|
||||||
{(routineRuns ?? []).map((run) => (
|
|
||||||
<div key={run.id} className="flex items-center justify-between px-3 py-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<Badge variant="outline" className="shrink-0">{run.source}</Badge>
|
|
||||||
<Badge variant={run.status === "failed" ? "destructive" : "secondary"} className="shrink-0">
|
|
||||||
{run.status.replaceAll("_", " ")}
|
|
||||||
</Badge>
|
|
||||||
{run.trigger && (
|
|
||||||
<span className="text-muted-foreground truncate">{run.trigger.label ?? run.trigger.kind}</span>
|
|
||||||
)}
|
|
||||||
{run.linkedIssue && (
|
|
||||||
<Link to={`/issues/${run.linkedIssue.identifier ?? run.linkedIssue.id}`} className="text-muted-foreground hover:underline truncate">
|
|
||||||
{run.linkedIssue.identifier ?? run.linkedIssue.id.slice(0, 8)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground shrink-0 ml-2">{timeAgo(run.triggeredAt)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="activity">
|
|
||||||
{(activity ?? []).length === 0 ? (
|
|
||||||
<p className="text-xs text-muted-foreground">No activity yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="border border-border rounded-lg divide-y divide-border">
|
|
||||||
{(activity ?? []).map((event) => (
|
|
||||||
<div key={event.id} className="flex items-center justify-between px-3 py-2 text-xs gap-4">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<span className="font-medium text-foreground/90 shrink-0">{event.action.replaceAll(".", " ")}</span>
|
|
||||||
{event.details && Object.keys(event.details).length > 0 && (
|
|
||||||
<span className="text-muted-foreground truncate">
|
|
||||||
{Object.entries(event.details).slice(0, 3).map(([key, value], i) => (
|
|
||||||
<span key={key}>
|
|
||||||
{i > 0 && <span className="mx-1 text-border">·</span>}
|
|
||||||
<span className="text-muted-foreground/70">{key.replaceAll("_", " ")}:</span>{" "}
|
|
||||||
{formatActivityDetailValue(value)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground/60 shrink-0">{timeAgo(event.createdAt)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<RoutineRunVariablesDialog
|
<RoutineRunVariablesDialog
|
||||||
open={runVariablesOpen}
|
open={runVariablesOpen}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue