@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,405 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type FormEvent } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage, type ResourceRef } from "@stigmer/sdk";
|
|
6
|
+
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
7
|
+
import { Popover } from "@base-ui/react/popover";
|
|
8
|
+
import { AgentPicker } from "../agent/AgentPicker.js";
|
|
9
|
+
import { Switch } from "../switch/Switch.js";
|
|
10
|
+
import { useStigmerPortalContainer } from "../portal-container.js";
|
|
11
|
+
import { CadenceField } from "./CadenceField.js";
|
|
12
|
+
import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
|
|
13
|
+
import { useCreateSchedule } from "./useCreateSchedule.js";
|
|
14
|
+
import { cadenceToCron, validateCron, type CadencePreset } from "./cadence.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public API
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** Props for {@link ScheduleForm}. */
|
|
21
|
+
export interface ScheduleFormProps {
|
|
22
|
+
/**
|
|
23
|
+
* Organization slug. Schedules are org-local: the server requires the
|
|
24
|
+
* target agent to live in this same org, so the agent picker is
|
|
25
|
+
* locked to org scope.
|
|
26
|
+
*/
|
|
27
|
+
readonly org: string;
|
|
28
|
+
/** Fired with the created schedule after a successful submit. */
|
|
29
|
+
readonly onComplete?: (schedule: Schedule) => void;
|
|
30
|
+
/** Fired when the user cancels. */
|
|
31
|
+
readonly onCancel?: () => void;
|
|
32
|
+
/** Additional CSS class names for the root form element. */
|
|
33
|
+
readonly className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** spec.proto pins AgentTarget.message to 8192 characters. */
|
|
37
|
+
const MESSAGE_MAX_LEN = 8192;
|
|
38
|
+
/** Show the remaining-characters counter once within this margin. */
|
|
39
|
+
const MESSAGE_COUNTER_THRESHOLD = 500;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Single-page form for creating a {@link Schedule}.
|
|
43
|
+
*
|
|
44
|
+
* Collects the four things a user decides about a schedule — which
|
|
45
|
+
* agent, what message, when ({@link CadenceField}, generating the cron
|
|
46
|
+
* expression from human-friendly presets), and whether it starts
|
|
47
|
+
* enabled — then creates it via {@link useCreateSchedule}.
|
|
48
|
+
*
|
|
49
|
+
* Enabled defaults to OFF, deliberately: it matches the proto/YAML
|
|
50
|
+
* default (an omitted `enabled` is false) and the platform's
|
|
51
|
+
* recommended workflow — create staged-disabled, validate with
|
|
52
|
+
* "Run now" on the detail page, then enable.
|
|
53
|
+
*
|
|
54
|
+
* Creation only. Editing an existing schedule must not flow through
|
|
55
|
+
* this form's curated input — see the note on {@link useCreateSchedule}.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <ScheduleForm
|
|
60
|
+
* org="acme"
|
|
61
|
+
* onComplete={(s) => navigate(`/library/schedules/${s.metadata?.org}/${s.metadata?.slug}`)}
|
|
62
|
+
* onCancel={() => navigate("/library/schedules")}
|
|
63
|
+
* />
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function ScheduleForm({
|
|
67
|
+
org,
|
|
68
|
+
onComplete,
|
|
69
|
+
onCancel,
|
|
70
|
+
className,
|
|
71
|
+
}: ScheduleFormProps) {
|
|
72
|
+
const { create, isCreating, error, clearError } = useCreateSchedule();
|
|
73
|
+
|
|
74
|
+
const [name, setName] = useState("");
|
|
75
|
+
const [agentRef, setAgentRef] = useState<ResourceRef | null>(null);
|
|
76
|
+
const [agentName, setAgentName] = useState<string | null>(null);
|
|
77
|
+
const [message, setMessage] = useState("");
|
|
78
|
+
const [cadence, setCadence] = useState<CadencePreset>({
|
|
79
|
+
kind: "daily",
|
|
80
|
+
hour: 9,
|
|
81
|
+
minute: 0,
|
|
82
|
+
});
|
|
83
|
+
const [timeZone, setTimeZone] = useState(browserTimeZone);
|
|
84
|
+
const [enabled, setEnabled] = useState(false);
|
|
85
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
86
|
+
|
|
87
|
+
const trimmedName = name.trim();
|
|
88
|
+
const trimmedMessage = message.trim();
|
|
89
|
+
const cron = cadenceToCron(cadence).trim();
|
|
90
|
+
const cronValid = cron !== "" && validateCron(cron) === null;
|
|
91
|
+
|
|
92
|
+
const canSubmit =
|
|
93
|
+
trimmedName !== "" &&
|
|
94
|
+
agentRef !== null &&
|
|
95
|
+
trimmedMessage !== "" &&
|
|
96
|
+
cronValid &&
|
|
97
|
+
!isCreating;
|
|
98
|
+
|
|
99
|
+
const handleAgentChange = useCallback((ref: ResourceRef | null) => {
|
|
100
|
+
setAgentRef(ref);
|
|
101
|
+
if (ref === null) setAgentName(null);
|
|
102
|
+
setPickerOpen(false);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const handleSubmit = useCallback(
|
|
106
|
+
async (e: FormEvent) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
if (!canSubmit || !agentRef) return;
|
|
109
|
+
|
|
110
|
+
clearError();
|
|
111
|
+
try {
|
|
112
|
+
const schedule = await create({
|
|
113
|
+
name: trimmedName,
|
|
114
|
+
org,
|
|
115
|
+
cron,
|
|
116
|
+
timeZone,
|
|
117
|
+
enabled,
|
|
118
|
+
agent: { agentRef, message: trimmedMessage },
|
|
119
|
+
});
|
|
120
|
+
onComplete?.(schedule);
|
|
121
|
+
} catch {
|
|
122
|
+
// error state is managed by useCreateSchedule
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[
|
|
126
|
+
canSubmit,
|
|
127
|
+
agentRef,
|
|
128
|
+
clearError,
|
|
129
|
+
create,
|
|
130
|
+
trimmedName,
|
|
131
|
+
org,
|
|
132
|
+
cron,
|
|
133
|
+
timeZone,
|
|
134
|
+
enabled,
|
|
135
|
+
trimmedMessage,
|
|
136
|
+
onComplete,
|
|
137
|
+
],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const remaining = MESSAGE_MAX_LEN - message.length;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<form
|
|
144
|
+
onSubmit={handleSubmit}
|
|
145
|
+
className={cn("max-w-2xl space-y-5", className)}
|
|
146
|
+
aria-label="New schedule"
|
|
147
|
+
>
|
|
148
|
+
{/* Name */}
|
|
149
|
+
<div className="space-y-1">
|
|
150
|
+
<label htmlFor="stgm-new-schedule-name" className={labelClasses}>
|
|
151
|
+
Name
|
|
152
|
+
</label>
|
|
153
|
+
<input
|
|
154
|
+
id="stgm-new-schedule-name"
|
|
155
|
+
type="text"
|
|
156
|
+
value={name}
|
|
157
|
+
onChange={(e) => setName(e.target.value)}
|
|
158
|
+
placeholder="e.g. daily-fee-reminders"
|
|
159
|
+
disabled={isCreating}
|
|
160
|
+
autoFocus
|
|
161
|
+
required
|
|
162
|
+
className={inputClasses}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Target agent */}
|
|
167
|
+
<div className="space-y-1">
|
|
168
|
+
<span id="stgm-new-schedule-agent-label" className={labelClasses}>
|
|
169
|
+
Agent to run
|
|
170
|
+
</span>
|
|
171
|
+
<Popover.Root open={pickerOpen} onOpenChange={setPickerOpen}>
|
|
172
|
+
<Popover.Trigger
|
|
173
|
+
disabled={isCreating}
|
|
174
|
+
aria-labelledby="stgm-new-schedule-agent-label"
|
|
175
|
+
className={cn(
|
|
176
|
+
"flex w-full items-center justify-between rounded-md border border-input bg-background px-2.5 py-1.5 text-left text-xs",
|
|
177
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
178
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
179
|
+
agentRef ? "text-foreground" : "text-muted-foreground",
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
<span className="truncate">
|
|
183
|
+
{agentRef
|
|
184
|
+
? (agentName ?? `${agentRef.org}/${agentRef.slug}`)
|
|
185
|
+
: "Choose an agent…"}
|
|
186
|
+
</span>
|
|
187
|
+
<ChevronIcon />
|
|
188
|
+
</Popover.Trigger>
|
|
189
|
+
<AgentPickerPopup
|
|
190
|
+
org={org}
|
|
191
|
+
value={agentRef}
|
|
192
|
+
onChange={handleAgentChange}
|
|
193
|
+
onDisplayNameResolved={(_key, resolvedName) =>
|
|
194
|
+
setAgentName(resolvedName)
|
|
195
|
+
}
|
|
196
|
+
disabled={isCreating}
|
|
197
|
+
/>
|
|
198
|
+
</Popover.Root>
|
|
199
|
+
<p className={hintClasses}>
|
|
200
|
+
Each fire runs this agent unattended in a fresh session. Only
|
|
201
|
+
agents in <span className="font-medium">{org}</span> can be
|
|
202
|
+
scheduled.
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Message */}
|
|
207
|
+
<div className="space-y-1">
|
|
208
|
+
<label htmlFor="stgm-new-schedule-message" className={labelClasses}>
|
|
209
|
+
Message
|
|
210
|
+
</label>
|
|
211
|
+
<textarea
|
|
212
|
+
id="stgm-new-schedule-message"
|
|
213
|
+
value={message}
|
|
214
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
215
|
+
placeholder="The instruction the agent receives on every fire — write it for a run with no human present."
|
|
216
|
+
disabled={isCreating}
|
|
217
|
+
required
|
|
218
|
+
rows={5}
|
|
219
|
+
maxLength={MESSAGE_MAX_LEN}
|
|
220
|
+
className={cn(inputClasses, "resize-y")}
|
|
221
|
+
/>
|
|
222
|
+
{remaining <= MESSAGE_COUNTER_THRESHOLD && (
|
|
223
|
+
<p className={hintClasses}>{remaining} characters left</p>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Cadence */}
|
|
228
|
+
<div className="space-y-1">
|
|
229
|
+
<span className={labelClasses}>Runs</span>
|
|
230
|
+
<CadenceField
|
|
231
|
+
value={cadence}
|
|
232
|
+
onChange={setCadence}
|
|
233
|
+
timeZone={timeZone}
|
|
234
|
+
disabled={isCreating}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Time zone */}
|
|
239
|
+
<div className="space-y-1">
|
|
240
|
+
<label htmlFor="stgm-new-schedule-tz" className={labelClasses}>
|
|
241
|
+
Time zone
|
|
242
|
+
</label>
|
|
243
|
+
<TimeZoneField
|
|
244
|
+
id="stgm-new-schedule-tz"
|
|
245
|
+
value={timeZone}
|
|
246
|
+
onChange={setTimeZone}
|
|
247
|
+
disabled={isCreating}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Enabled */}
|
|
252
|
+
<div className="flex items-start gap-3">
|
|
253
|
+
<Switch
|
|
254
|
+
id="stgm-new-schedule-enabled"
|
|
255
|
+
checked={enabled}
|
|
256
|
+
onCheckedChange={setEnabled}
|
|
257
|
+
disabled={isCreating}
|
|
258
|
+
aria-labelledby="stgm-new-schedule-enabled-label"
|
|
259
|
+
/>
|
|
260
|
+
<div className="space-y-0.5">
|
|
261
|
+
<label
|
|
262
|
+
id="stgm-new-schedule-enabled-label"
|
|
263
|
+
htmlFor="stgm-new-schedule-enabled"
|
|
264
|
+
className={labelClasses}
|
|
265
|
+
>
|
|
266
|
+
Enabled
|
|
267
|
+
</label>
|
|
268
|
+
<p className={hintClasses}>
|
|
269
|
+
Leave off to create the schedule staged: validate it with
|
|
270
|
+
“Run now” on the schedule page, then enable it there.
|
|
271
|
+
</p>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{error && (
|
|
276
|
+
<p className="text-xs text-destructive" role="alert">
|
|
277
|
+
{getUserMessage(error)}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<button
|
|
283
|
+
type="submit"
|
|
284
|
+
disabled={!canSubmit}
|
|
285
|
+
className={cn(
|
|
286
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium",
|
|
287
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
288
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
289
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
290
|
+
)}
|
|
291
|
+
>
|
|
292
|
+
{isCreating && <SpinnerIcon />}
|
|
293
|
+
Create schedule
|
|
294
|
+
</button>
|
|
295
|
+
|
|
296
|
+
{onCancel && (
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={onCancel}
|
|
300
|
+
disabled={isCreating}
|
|
301
|
+
className={cn(
|
|
302
|
+
"rounded-md px-3 py-1.5 text-sm",
|
|
303
|
+
"text-muted-foreground hover:bg-accent-hover hover:text-foreground",
|
|
304
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
305
|
+
)}
|
|
306
|
+
>
|
|
307
|
+
Cancel
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</form>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Private helpers
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
const labelClasses = "block text-xs font-medium text-foreground";
|
|
320
|
+
const hintClasses = "text-[0.65rem] text-muted-foreground";
|
|
321
|
+
const inputClasses = cn(
|
|
322
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
323
|
+
"placeholder:text-muted-foreground",
|
|
324
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
325
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
function AgentPickerPopup({
|
|
329
|
+
org,
|
|
330
|
+
value,
|
|
331
|
+
onChange,
|
|
332
|
+
onDisplayNameResolved,
|
|
333
|
+
disabled,
|
|
334
|
+
}: {
|
|
335
|
+
readonly org: string;
|
|
336
|
+
readonly value: ResourceRef | null;
|
|
337
|
+
readonly onChange: (ref: ResourceRef | null) => void;
|
|
338
|
+
readonly onDisplayNameResolved: (key: string, name: string) => void;
|
|
339
|
+
readonly disabled?: boolean;
|
|
340
|
+
}) {
|
|
341
|
+
const portalContainer = useStigmerPortalContainer();
|
|
342
|
+
return (
|
|
343
|
+
<Popover.Portal container={portalContainer}>
|
|
344
|
+
<Popover.Positioner sideOffset={4} align="start">
|
|
345
|
+
<Popover.Popup
|
|
346
|
+
className={cn(
|
|
347
|
+
"z-popover overflow-x-hidden overflow-y-auto rounded-lg border border-border",
|
|
348
|
+
"bg-popover p-3 text-popover-foreground shadow-md",
|
|
349
|
+
"max-h-[60vh]",
|
|
350
|
+
)}
|
|
351
|
+
>
|
|
352
|
+
<AgentPicker
|
|
353
|
+
org={org}
|
|
354
|
+
value={value}
|
|
355
|
+
onChange={onChange}
|
|
356
|
+
onDisplayNameResolved={onDisplayNameResolved}
|
|
357
|
+
// Schedules are org-local: the server rejects a target
|
|
358
|
+
// agent outside the schedule's org, so cross-org browsing
|
|
359
|
+
// could only offer choices that fail at submit.
|
|
360
|
+
scope="org"
|
|
361
|
+
showScopeToggle={false}
|
|
362
|
+
disabled={disabled}
|
|
363
|
+
/>
|
|
364
|
+
</Popover.Popup>
|
|
365
|
+
</Popover.Positioner>
|
|
366
|
+
</Popover.Portal>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ChevronIcon() {
|
|
371
|
+
return (
|
|
372
|
+
<svg
|
|
373
|
+
width="12"
|
|
374
|
+
height="12"
|
|
375
|
+
viewBox="0 0 16 16"
|
|
376
|
+
fill="none"
|
|
377
|
+
stroke="currentColor"
|
|
378
|
+
strokeWidth="1.5"
|
|
379
|
+
strokeLinecap="round"
|
|
380
|
+
strokeLinejoin="round"
|
|
381
|
+
className="shrink-0 text-muted-foreground"
|
|
382
|
+
aria-hidden="true"
|
|
383
|
+
>
|
|
384
|
+
<path d="M4 6l4 4 4-4" />
|
|
385
|
+
</svg>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function SpinnerIcon() {
|
|
390
|
+
return (
|
|
391
|
+
<svg
|
|
392
|
+
width="14"
|
|
393
|
+
height="14"
|
|
394
|
+
viewBox="0 0 16 16"
|
|
395
|
+
fill="none"
|
|
396
|
+
stroke="currentColor"
|
|
397
|
+
strokeWidth="2"
|
|
398
|
+
strokeLinecap="round"
|
|
399
|
+
className="animate-spin"
|
|
400
|
+
aria-hidden="true"
|
|
401
|
+
>
|
|
402
|
+
<path d="M8 2a6 6 0 1 0 6 6" />
|
|
403
|
+
</svg>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@stigmer/theme";
|
|
4
|
+
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
5
|
+
import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
|
|
6
|
+
import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
|
|
7
|
+
import { deriveScheduleState } from "./scheduleState.js";
|
|
8
|
+
import { useResumeSchedule } from "./useResumeSchedule.js";
|
|
9
|
+
import { useTriggerSchedule } from "./useTriggerSchedule.js";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link ScheduleRowActions}. */
|
|
12
|
+
export interface ScheduleRowActionsProps {
|
|
13
|
+
/** The schedule this row represents. */
|
|
14
|
+
readonly schedule: Schedule;
|
|
15
|
+
/** Called after a successful action so the list can refetch. */
|
|
16
|
+
readonly onChanged?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compact per-row actions for a schedule list: **Run now** on an active
|
|
21
|
+
* schedule (confirmation-gated — it starts a real, billable execution)
|
|
22
|
+
* and **Resume** on a platform-paused one. Exactly one action renders
|
|
23
|
+
* at a time — the one that is currently meaningful; an owner-disabled
|
|
24
|
+
* schedule renders neither (its remedy lives on the detail page).
|
|
25
|
+
*
|
|
26
|
+
* Shared by every console's schedule workbench via `renderItemAction`
|
|
27
|
+
* so row behavior never diverges between apps (DD-016).
|
|
28
|
+
*/
|
|
29
|
+
export function ScheduleRowActions({
|
|
30
|
+
schedule,
|
|
31
|
+
onChanged,
|
|
32
|
+
}: ScheduleRowActionsProps) {
|
|
33
|
+
const { resumeSchedule, isResuming } = useResumeSchedule();
|
|
34
|
+
const { triggerSchedule, isTriggering } = useTriggerSchedule();
|
|
35
|
+
const { confirmState, confirm, handleConfirm, handleCancel } =
|
|
36
|
+
useConfirmAction();
|
|
37
|
+
|
|
38
|
+
const info = deriveScheduleState(schedule.spec, schedule.status);
|
|
39
|
+
const scheduleId = schedule.metadata?.id ?? "";
|
|
40
|
+
const name = schedule.metadata?.name || schedule.metadata?.slug || "schedule";
|
|
41
|
+
|
|
42
|
+
const handleTrigger = async () => {
|
|
43
|
+
const confirmed = await confirm({
|
|
44
|
+
title: "Run this schedule now?",
|
|
45
|
+
description:
|
|
46
|
+
`"${name}" starts a real agent execution immediately, outside ` +
|
|
47
|
+
"the cron cadence.",
|
|
48
|
+
confirmLabel: "Start run",
|
|
49
|
+
variant: "default",
|
|
50
|
+
});
|
|
51
|
+
if (!confirmed) return;
|
|
52
|
+
await triggerSchedule(scheduleId);
|
|
53
|
+
onChanged?.();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleResume = async () => {
|
|
57
|
+
await resumeSchedule(scheduleId);
|
|
58
|
+
onChanged?.();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (info.state === "disabled") return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
// Row actions must never bubble into the row's navigate-to-detail
|
|
65
|
+
// click handler. The span is a non-interactive event fence — the
|
|
66
|
+
// real interactive element (with keyboard support) is the button.
|
|
67
|
+
<span onClick={(e) => e.stopPropagation()}>
|
|
68
|
+
{info.state === "active" ? (
|
|
69
|
+
<RowActionButton
|
|
70
|
+
label={`Run ${name} now`}
|
|
71
|
+
busy={isTriggering}
|
|
72
|
+
onClick={() => void handleTrigger()}
|
|
73
|
+
>
|
|
74
|
+
Run now
|
|
75
|
+
</RowActionButton>
|
|
76
|
+
) : (
|
|
77
|
+
<RowActionButton
|
|
78
|
+
label={`Resume ${name}`}
|
|
79
|
+
busy={isResuming}
|
|
80
|
+
onClick={() => void handleResume()}
|
|
81
|
+
>
|
|
82
|
+
Resume
|
|
83
|
+
</RowActionButton>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{confirmState && (
|
|
87
|
+
<ConfirmDialog
|
|
88
|
+
state={confirmState}
|
|
89
|
+
onConfirm={handleConfirm}
|
|
90
|
+
onCancel={handleCancel}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function RowActionButton({
|
|
98
|
+
label,
|
|
99
|
+
busy,
|
|
100
|
+
onClick,
|
|
101
|
+
children,
|
|
102
|
+
}: {
|
|
103
|
+
readonly label: string;
|
|
104
|
+
readonly busy: boolean;
|
|
105
|
+
readonly onClick: () => void;
|
|
106
|
+
readonly children: React.ReactNode;
|
|
107
|
+
}) {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
aria-label={label}
|
|
112
|
+
onClick={onClick}
|
|
113
|
+
disabled={busy}
|
|
114
|
+
className={cn(
|
|
115
|
+
"rounded-md border border-input bg-background px-2 py-1 text-xs font-medium text-foreground",
|
|
116
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
117
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
118
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
}
|