@stigmer/react 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +9 -1
- package/index.js.map +1 -1
- package/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +58 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +236 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +43 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +129 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- package/schedule/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +31 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +16 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- package/schedule/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +28 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +43 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- package/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- package/src/agent/AgentPicker.tsx +13 -1
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
- package/src/index.ts +65 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +583 -0
- package/src/schedule/ScheduleForm.tsx +405 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +57 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +65 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- package/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +1 -1
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
7
|
+
import { formatRelativeTime } from "../activity/format-relative-time.js";
|
|
8
|
+
import { ErrorMessage } from "../error/ErrorMessage.js";
|
|
9
|
+
import { EditResourceYamlDialog } from "../manifest/EditResourceYamlDialog.js";
|
|
10
|
+
import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
|
|
11
|
+
import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
|
|
12
|
+
import { Section } from "../resource-detail/Section.js";
|
|
13
|
+
import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
|
|
14
|
+
import { useCopyResource } from "../resource-detail/useCopyResource.js";
|
|
15
|
+
import { useDeleteResource } from "../resource-detail/useDeleteResource.js";
|
|
16
|
+
import type { DetailAction, ResourceHeaderMeta } from "../resource-detail/types.js";
|
|
17
|
+
import { useExportResource } from "../library/useExportResource.js";
|
|
18
|
+
import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
|
|
19
|
+
import { useSchedule } from "./useSchedule.js";
|
|
20
|
+
import { useResumeSchedule } from "./useResumeSchedule.js";
|
|
21
|
+
import { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
|
|
22
|
+
import { useTriggerSchedule } from "./useTriggerSchedule.js";
|
|
23
|
+
|
|
24
|
+
/** Props for {@link ScheduleDetailView}. */
|
|
25
|
+
export interface ScheduleDetailViewProps {
|
|
26
|
+
/** Organization slug. */
|
|
27
|
+
readonly org: string;
|
|
28
|
+
/** Schedule slug. */
|
|
29
|
+
readonly slug: string;
|
|
30
|
+
/**
|
|
31
|
+
* Called when the user activates the target-agent reference.
|
|
32
|
+
* Navigation is the consumer's concern (DD-004) — the Console pushes
|
|
33
|
+
* a Library route; an embedding host does whatever fits.
|
|
34
|
+
*/
|
|
35
|
+
readonly onNavigateToAgent?: (org: string, slug: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Called when the user activates the last-execution reference
|
|
38
|
+
* (`aex_…`). When omitted, the id renders as plain text.
|
|
39
|
+
*/
|
|
40
|
+
readonly onNavigateToExecution?: (executionId: string) => void;
|
|
41
|
+
/** Called after a successful delete (navigate back to the list). */
|
|
42
|
+
readonly onDeleted?: () => void;
|
|
43
|
+
/** Called when the schedule loads or reloads (e.g. breadcrumb label sync). */
|
|
44
|
+
readonly onResourceLoad?: (schedule: Schedule) => void;
|
|
45
|
+
/**
|
|
46
|
+
* The instant "next fire" countdowns are computed against.
|
|
47
|
+
* Injectable for deterministic tests and Scenar fixtures.
|
|
48
|
+
* @default new Date() at render
|
|
49
|
+
*/
|
|
50
|
+
readonly now?: Date;
|
|
51
|
+
/** Additional CSS classes for the root container. */
|
|
52
|
+
readonly className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Self-contained detail view for a Schedule (stigmer/stigmer#352).
|
|
57
|
+
*
|
|
58
|
+
* The view's one non-negotiable is rendering the two stop-levers
|
|
59
|
+
* distinctly, each with its inline remedy:
|
|
60
|
+
*
|
|
61
|
+
* - **Disabled** (owner's `spec.enabled` switch) — banner with an
|
|
62
|
+
* Enable action (a lossless full-proto re-apply).
|
|
63
|
+
* - **Paused** (platform's failure-streak latch,
|
|
64
|
+
* `status.paused_reason`) — banner showing the reason with a Resume
|
|
65
|
+
* action (the one path that clears the pause).
|
|
66
|
+
*
|
|
67
|
+
* Trigger ("Run now") starts a real, billable execution and is gated
|
|
68
|
+
* behind a confirmation; it is disabled while the schedule cannot fire
|
|
69
|
+
* (the banner names the remedy). Edit YAML, Export, and Delete round
|
|
70
|
+
* out the action set.
|
|
71
|
+
*
|
|
72
|
+
* Handles loading, error, and not-found states automatically.
|
|
73
|
+
* Zero Console dependencies — safe for platform builder embedding.
|
|
74
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
75
|
+
*/
|
|
76
|
+
export function ScheduleDetailView({
|
|
77
|
+
org,
|
|
78
|
+
slug,
|
|
79
|
+
onNavigateToAgent,
|
|
80
|
+
onNavigateToExecution,
|
|
81
|
+
onDeleted,
|
|
82
|
+
onResourceLoad,
|
|
83
|
+
now,
|
|
84
|
+
className,
|
|
85
|
+
}: ScheduleDetailViewProps) {
|
|
86
|
+
const { schedule, isLoading, error, refetch } = useSchedule(org, slug);
|
|
87
|
+
|
|
88
|
+
const { resumeSchedule, isResuming } = useResumeSchedule();
|
|
89
|
+
const { triggerSchedule, isTriggering } = useTriggerSchedule();
|
|
90
|
+
const { setEnabled, isPending: isToggling } = useSetScheduleEnabled();
|
|
91
|
+
const { deleteResource, isDeleting } = useDeleteResource(
|
|
92
|
+
"schedule",
|
|
93
|
+
schedule?.metadata?.id ?? null,
|
|
94
|
+
schedule?.metadata?.name,
|
|
95
|
+
);
|
|
96
|
+
const { confirmState, confirm, handleConfirm, handleCancel } =
|
|
97
|
+
useConfirmAction();
|
|
98
|
+
const { copyId, copyQualifiedSlug } = useCopyResource();
|
|
99
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
100
|
+
|
|
101
|
+
const { copyYaml, downloadYaml } = useExportResource({
|
|
102
|
+
kind: "Schedule",
|
|
103
|
+
resource: schedule,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (schedule) onResourceLoad?.(schedule);
|
|
108
|
+
}, [schedule, onResourceLoad]);
|
|
109
|
+
|
|
110
|
+
if (isLoading) return <LoadingSkeleton className={className} />;
|
|
111
|
+
if (error)
|
|
112
|
+
return <ErrorMessage error={error} retry={refetch} className={className} />;
|
|
113
|
+
if (!schedule) return <NotFoundState className={className} />;
|
|
114
|
+
|
|
115
|
+
const meta = schedule.metadata;
|
|
116
|
+
const spec = schedule.spec;
|
|
117
|
+
const status = schedule.status;
|
|
118
|
+
const specAudit = status?.audit?.specAudit;
|
|
119
|
+
const stateInfo = deriveScheduleState(spec, status);
|
|
120
|
+
const renderNow = now ?? new Date();
|
|
121
|
+
|
|
122
|
+
const scheduleId = meta?.id ?? "";
|
|
123
|
+
const target = spec?.target?.case === "agent" ? spec.target.value : undefined;
|
|
124
|
+
|
|
125
|
+
const handleResume = async () => {
|
|
126
|
+
await resumeSchedule(scheduleId);
|
|
127
|
+
refetch();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleToggleEnabled = async () => {
|
|
131
|
+
await setEnabled(schedule, !spec?.enabled);
|
|
132
|
+
refetch();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleTrigger = async () => {
|
|
136
|
+
const confirmed = await confirm({
|
|
137
|
+
title: "Run this schedule now?",
|
|
138
|
+
description:
|
|
139
|
+
"This starts a real agent execution immediately, outside the cron " +
|
|
140
|
+
"cadence. The run appears as the schedule's last execution.",
|
|
141
|
+
confirmLabel: "Start run",
|
|
142
|
+
variant: "default",
|
|
143
|
+
});
|
|
144
|
+
if (!confirmed) return;
|
|
145
|
+
await triggerSchedule(scheduleId);
|
|
146
|
+
refetch();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleDelete = async () => {
|
|
150
|
+
const confirmed = await confirm({
|
|
151
|
+
title: "Delete this schedule?",
|
|
152
|
+
description:
|
|
153
|
+
`"${meta?.name ?? slug}" will stop firing and be permanently ` +
|
|
154
|
+
"removed. Past executions are not affected. This cannot be undone.",
|
|
155
|
+
confirmLabel: "Delete",
|
|
156
|
+
variant: "destructive",
|
|
157
|
+
});
|
|
158
|
+
if (!confirmed) return;
|
|
159
|
+
await deleteResource();
|
|
160
|
+
onDeleted?.();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const primaryAction: DetailAction = {
|
|
164
|
+
id: "trigger",
|
|
165
|
+
label: "Run now",
|
|
166
|
+
onAction: () => void handleTrigger(),
|
|
167
|
+
// A disabled or paused schedule refuses to trigger server-side; the
|
|
168
|
+
// banner above names the remedy, so the button reflects it too
|
|
169
|
+
// (error prevention over error recovery).
|
|
170
|
+
disabled: stateInfo.state !== "active" || isTriggering,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const actions: DetailAction[] = [
|
|
174
|
+
...(stateInfo.isPaused
|
|
175
|
+
? [
|
|
176
|
+
{
|
|
177
|
+
id: "resume",
|
|
178
|
+
label: "Resume schedule",
|
|
179
|
+
onAction: () => void handleResume(),
|
|
180
|
+
disabled: isResuming,
|
|
181
|
+
group: "state",
|
|
182
|
+
},
|
|
183
|
+
]
|
|
184
|
+
: []),
|
|
185
|
+
{
|
|
186
|
+
id: "toggle-enabled",
|
|
187
|
+
label: spec?.enabled ? "Disable schedule" : "Enable schedule",
|
|
188
|
+
onAction: () => void handleToggleEnabled(),
|
|
189
|
+
disabled: isToggling,
|
|
190
|
+
group: "state",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "copy-id",
|
|
194
|
+
label: "Copy ID",
|
|
195
|
+
onAction: () => void copyId(scheduleId),
|
|
196
|
+
group: "clipboard",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "copy-slug",
|
|
200
|
+
label: "Copy slug",
|
|
201
|
+
onAction: () => void copyQualifiedSlug(org, slug),
|
|
202
|
+
group: "clipboard",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "edit-yaml",
|
|
206
|
+
label: "Edit YAML",
|
|
207
|
+
onAction: () => setEditOpen(true),
|
|
208
|
+
group: "edit",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "copy-yaml",
|
|
212
|
+
label: "Copy YAML",
|
|
213
|
+
onAction: () => void copyYaml(),
|
|
214
|
+
group: "export",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "download-yaml",
|
|
218
|
+
label: "Download YAML",
|
|
219
|
+
onAction: downloadYaml,
|
|
220
|
+
group: "export",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "delete",
|
|
224
|
+
label: "Delete",
|
|
225
|
+
variant: "destructive",
|
|
226
|
+
onAction: () => void handleDelete(),
|
|
227
|
+
disabled: isDeleting,
|
|
228
|
+
group: "danger",
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const headerMeta: ResourceHeaderMeta = {
|
|
233
|
+
name: meta?.name || meta?.slug || "Untitled",
|
|
234
|
+
id: scheduleId,
|
|
235
|
+
org: meta?.org,
|
|
236
|
+
slug: meta?.slug,
|
|
237
|
+
icon: <ScheduleIcon className="size-6 text-muted-foreground" />,
|
|
238
|
+
createdAt: specAudit?.createdAt ? timestampDate(specAudit.createdAt) : null,
|
|
239
|
+
updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
|
|
240
|
+
status: stateInfo.phase,
|
|
241
|
+
statusLabel: stateInfo.label,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const headerBanner =
|
|
245
|
+
stateInfo.state === "disabled" ? (
|
|
246
|
+
<StateBanner
|
|
247
|
+
title="Schedule is disabled"
|
|
248
|
+
onAction={() => void handleToggleEnabled()}
|
|
249
|
+
actionLabel="Enable schedule"
|
|
250
|
+
actionBusy={isToggling}
|
|
251
|
+
>
|
|
252
|
+
The owner switch (<code className="font-mono">spec.enabled</code>) is
|
|
253
|
+
off — this schedule will not fire.
|
|
254
|
+
{stateInfo.isPaused && (
|
|
255
|
+
<>
|
|
256
|
+
{" "}
|
|
257
|
+
It is also paused by the platform ({status?.pausedReason}); after
|
|
258
|
+
enabling, use Resume to clear the pause.
|
|
259
|
+
</>
|
|
260
|
+
)}
|
|
261
|
+
</StateBanner>
|
|
262
|
+
) : stateInfo.state === "paused" ? (
|
|
263
|
+
<StateBanner
|
|
264
|
+
title="Paused by the platform"
|
|
265
|
+
onAction={() => void handleResume()}
|
|
266
|
+
actionLabel="Resume schedule"
|
|
267
|
+
actionBusy={isResuming}
|
|
268
|
+
>
|
|
269
|
+
{status?.pausedReason} — resuming clears the pause and the failure
|
|
270
|
+
streak. Re-applying the manifest does not.
|
|
271
|
+
</StateBanner>
|
|
272
|
+
) : undefined;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<>
|
|
276
|
+
<ResourceDetailShell
|
|
277
|
+
header={headerMeta}
|
|
278
|
+
headerBanner={headerBanner}
|
|
279
|
+
primaryAction={primaryAction}
|
|
280
|
+
actions={actions}
|
|
281
|
+
className={className}
|
|
282
|
+
>
|
|
283
|
+
<div className="flex flex-col gap-6">
|
|
284
|
+
<Section title="Definition">
|
|
285
|
+
<dl className="divide-y divide-border">
|
|
286
|
+
<DetailRow label="Cron">
|
|
287
|
+
<code className="font-mono text-sm text-foreground">
|
|
288
|
+
{spec?.cron || "—"}
|
|
289
|
+
</code>
|
|
290
|
+
</DetailRow>
|
|
291
|
+
<DetailRow label="Time zone">
|
|
292
|
+
<span className="text-sm text-foreground">
|
|
293
|
+
{spec?.timeZone || "—"}
|
|
294
|
+
</span>
|
|
295
|
+
</DetailRow>
|
|
296
|
+
<DetailRow label="Target agent">
|
|
297
|
+
{target?.agentRef ? (
|
|
298
|
+
<ReferenceLink
|
|
299
|
+
label={`${target.agentRef.org}/${target.agentRef.slug}`}
|
|
300
|
+
onNavigate={
|
|
301
|
+
onNavigateToAgent
|
|
302
|
+
? () =>
|
|
303
|
+
onNavigateToAgent(
|
|
304
|
+
target.agentRef!.org,
|
|
305
|
+
target.agentRef!.slug,
|
|
306
|
+
)
|
|
307
|
+
: undefined
|
|
308
|
+
}
|
|
309
|
+
/>
|
|
310
|
+
) : (
|
|
311
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
312
|
+
)}
|
|
313
|
+
</DetailRow>
|
|
314
|
+
<DetailRow label="Message">
|
|
315
|
+
<p className="whitespace-pre-wrap break-words text-sm text-foreground">
|
|
316
|
+
{target?.message || "—"}
|
|
317
|
+
</p>
|
|
318
|
+
</DetailRow>
|
|
319
|
+
</dl>
|
|
320
|
+
</Section>
|
|
321
|
+
|
|
322
|
+
<Section title="Status">
|
|
323
|
+
<dl className="divide-y divide-border">
|
|
324
|
+
<DetailRow label="Next fire">
|
|
325
|
+
<span className="text-sm text-foreground">
|
|
326
|
+
{stateInfo.state === "active" && status?.nextFireAt
|
|
327
|
+
? formatNextFire(timestampDate(status.nextFireAt), renderNow)
|
|
328
|
+
: "—"}
|
|
329
|
+
</span>
|
|
330
|
+
</DetailRow>
|
|
331
|
+
<DetailRow label="Last fired">
|
|
332
|
+
<span className="text-sm text-foreground">
|
|
333
|
+
{status?.lastFireAt
|
|
334
|
+
? formatRelativeTime(
|
|
335
|
+
timestampDate(status.lastFireAt),
|
|
336
|
+
renderNow,
|
|
337
|
+
)
|
|
338
|
+
: "Never"}
|
|
339
|
+
</span>
|
|
340
|
+
</DetailRow>
|
|
341
|
+
<DetailRow label="Last execution">
|
|
342
|
+
{status?.lastExecutionId ? (
|
|
343
|
+
<ReferenceLink
|
|
344
|
+
label={status.lastExecutionId}
|
|
345
|
+
mono
|
|
346
|
+
onNavigate={
|
|
347
|
+
onNavigateToExecution
|
|
348
|
+
? () => onNavigateToExecution(status.lastExecutionId)
|
|
349
|
+
: undefined
|
|
350
|
+
}
|
|
351
|
+
/>
|
|
352
|
+
) : (
|
|
353
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
354
|
+
)}
|
|
355
|
+
</DetailRow>
|
|
356
|
+
<DetailRow label="Consecutive failures">
|
|
357
|
+
<span
|
|
358
|
+
className={cn(
|
|
359
|
+
"text-sm",
|
|
360
|
+
(status?.consecutiveFailures ?? 0) > 0
|
|
361
|
+
? "font-medium text-warning"
|
|
362
|
+
: "text-foreground",
|
|
363
|
+
)}
|
|
364
|
+
>
|
|
365
|
+
{status?.consecutiveFailures ?? 0}
|
|
366
|
+
</span>
|
|
367
|
+
</DetailRow>
|
|
368
|
+
</dl>
|
|
369
|
+
</Section>
|
|
370
|
+
</div>
|
|
371
|
+
</ResourceDetailShell>
|
|
372
|
+
|
|
373
|
+
<ConfirmDialog
|
|
374
|
+
state={confirmState}
|
|
375
|
+
onConfirm={handleConfirm}
|
|
376
|
+
onCancel={handleCancel}
|
|
377
|
+
/>
|
|
378
|
+
|
|
379
|
+
{/* Mounted on demand: the editor serializes the resource and pulls
|
|
380
|
+
the CodeMirror chunk, neither of which should cost anything
|
|
381
|
+
until the action is actually invoked (DD-013). The dialog
|
|
382
|
+
resets its edit state on open, so unmount-on-close loses
|
|
383
|
+
nothing. */}
|
|
384
|
+
{editOpen && (
|
|
385
|
+
<EditResourceYamlDialog
|
|
386
|
+
open={editOpen}
|
|
387
|
+
onOpenChange={setEditOpen}
|
|
388
|
+
resource={schedule}
|
|
389
|
+
onApplied={() => refetch()}
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
</>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// State banner — one lever, one remedy, inline
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
function StateBanner({
|
|
401
|
+
title,
|
|
402
|
+
children,
|
|
403
|
+
actionLabel,
|
|
404
|
+
onAction,
|
|
405
|
+
actionBusy,
|
|
406
|
+
}: {
|
|
407
|
+
readonly title: string;
|
|
408
|
+
readonly children: React.ReactNode;
|
|
409
|
+
readonly actionLabel: string;
|
|
410
|
+
readonly onAction: () => void;
|
|
411
|
+
readonly actionBusy: boolean;
|
|
412
|
+
}) {
|
|
413
|
+
return (
|
|
414
|
+
<div
|
|
415
|
+
role="status"
|
|
416
|
+
className="flex items-start gap-2.5 rounded-lg border border-warning/30 bg-warning/5 px-4 py-3"
|
|
417
|
+
>
|
|
418
|
+
<WarningIcon className="mt-0.5 size-4 shrink-0 text-warning" />
|
|
419
|
+
<div className="min-w-0 flex-1">
|
|
420
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
421
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{children}</p>
|
|
422
|
+
</div>
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
onClick={onAction}
|
|
426
|
+
disabled={actionBusy}
|
|
427
|
+
className={cn(
|
|
428
|
+
"shrink-0 rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground",
|
|
429
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
430
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
431
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
432
|
+
)}
|
|
433
|
+
>
|
|
434
|
+
{actionLabel}
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Detail rows and references
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function DetailRow({
|
|
445
|
+
label,
|
|
446
|
+
children,
|
|
447
|
+
}: {
|
|
448
|
+
readonly label: string;
|
|
449
|
+
readonly children: React.ReactNode;
|
|
450
|
+
}) {
|
|
451
|
+
return (
|
|
452
|
+
<div className="flex items-start gap-4 px-4 py-2.5">
|
|
453
|
+
<dt className="w-40 shrink-0 pt-0.5 text-xs font-medium text-muted-foreground">
|
|
454
|
+
{label}
|
|
455
|
+
</dt>
|
|
456
|
+
<dd className="min-w-0 flex-1">{children}</dd>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function ReferenceLink({
|
|
462
|
+
label,
|
|
463
|
+
onNavigate,
|
|
464
|
+
mono,
|
|
465
|
+
}: {
|
|
466
|
+
readonly label: string;
|
|
467
|
+
readonly onNavigate?: () => void;
|
|
468
|
+
readonly mono?: boolean;
|
|
469
|
+
}) {
|
|
470
|
+
const textClass = cn("text-sm", mono && "font-mono text-xs");
|
|
471
|
+
if (!onNavigate) {
|
|
472
|
+
return <span className={cn(textClass, "text-foreground")}>{label}</span>;
|
|
473
|
+
}
|
|
474
|
+
return (
|
|
475
|
+
<button
|
|
476
|
+
type="button"
|
|
477
|
+
onClick={onNavigate}
|
|
478
|
+
className={cn(
|
|
479
|
+
textClass,
|
|
480
|
+
"text-primary underline-offset-2 hover:underline",
|
|
481
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
|
|
482
|
+
)}
|
|
483
|
+
>
|
|
484
|
+
{label}
|
|
485
|
+
</button>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Non-happy states
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
function LoadingSkeleton({ className }: { readonly className?: string }) {
|
|
494
|
+
return (
|
|
495
|
+
<div
|
|
496
|
+
className={cn("flex flex-col gap-6", className)}
|
|
497
|
+
aria-busy="true"
|
|
498
|
+
aria-label="Loading schedule details"
|
|
499
|
+
>
|
|
500
|
+
<div className="flex items-start gap-3">
|
|
501
|
+
<div className="mt-1 size-6 shrink-0 animate-pulse rounded bg-muted" />
|
|
502
|
+
<div className="flex-1 space-y-2">
|
|
503
|
+
<div className="h-5 w-48 animate-pulse rounded bg-muted" />
|
|
504
|
+
<div className="h-3 w-64 animate-pulse rounded bg-muted" />
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
<div className="space-y-2">
|
|
508
|
+
<div className="h-3 w-28 animate-pulse rounded bg-muted" />
|
|
509
|
+
<div
|
|
510
|
+
className="animate-pulse rounded-lg border border-border bg-muted-faint"
|
|
511
|
+
style={{ height: "160px" }}
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
<div className="space-y-2">
|
|
515
|
+
<div className="h-3 w-20 animate-pulse rounded bg-muted" />
|
|
516
|
+
<div
|
|
517
|
+
className="animate-pulse rounded-lg border border-border bg-muted-faint"
|
|
518
|
+
style={{ height: "160px" }}
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function NotFoundState({ className }: { readonly className?: string }) {
|
|
526
|
+
return (
|
|
527
|
+
<div
|
|
528
|
+
role="status"
|
|
529
|
+
className={cn("flex flex-col items-center gap-2 py-12 text-center", className)}
|
|
530
|
+
>
|
|
531
|
+
<ScheduleIcon className="size-10 text-muted-foreground-faint" />
|
|
532
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
533
|
+
Schedule not found
|
|
534
|
+
</p>
|
|
535
|
+
<p className="text-xs text-muted-foreground-subtle">
|
|
536
|
+
This schedule doesn't exist or you don't have access to it.
|
|
537
|
+
</p>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Icons (inline SVG — no icon-library dependency)
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
/** Clock-with-arrow icon shared by the schedule views. */
|
|
547
|
+
export function ScheduleIcon({ className }: { readonly className?: string }) {
|
|
548
|
+
return (
|
|
549
|
+
<svg
|
|
550
|
+
className={className}
|
|
551
|
+
viewBox="0 0 24 24"
|
|
552
|
+
fill="none"
|
|
553
|
+
stroke="currentColor"
|
|
554
|
+
strokeWidth="1.5"
|
|
555
|
+
strokeLinecap="round"
|
|
556
|
+
strokeLinejoin="round"
|
|
557
|
+
aria-hidden="true"
|
|
558
|
+
>
|
|
559
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
|
|
560
|
+
<path d="M21 3v5h-5" />
|
|
561
|
+
<path d="M12 7v5l3 3" />
|
|
562
|
+
</svg>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function WarningIcon({ className }: { readonly className?: string }) {
|
|
567
|
+
return (
|
|
568
|
+
<svg
|
|
569
|
+
className={className}
|
|
570
|
+
viewBox="0 0 16 16"
|
|
571
|
+
fill="none"
|
|
572
|
+
stroke="currentColor"
|
|
573
|
+
strokeWidth="1.5"
|
|
574
|
+
strokeLinecap="round"
|
|
575
|
+
strokeLinejoin="round"
|
|
576
|
+
aria-hidden="true"
|
|
577
|
+
>
|
|
578
|
+
<path d="M8 1.5 15 14H1L8 1.5Z" />
|
|
579
|
+
<path d="M8 6v4" />
|
|
580
|
+
<path d="M8 12.2v.05" />
|
|
581
|
+
</svg>
|
|
582
|
+
);
|
|
583
|
+
}
|