@growthub/cli 0.14.9 → 0.14.11
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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
- package/dist/index.js +3024 -4191
- package/package.json +1 -1
|
@@ -6,7 +6,6 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|
|
6
6
|
import {
|
|
7
7
|
ArrowDown,
|
|
8
8
|
ArrowUp,
|
|
9
|
-
ArrowUpCircle,
|
|
10
9
|
Bot,
|
|
11
10
|
Code,
|
|
12
11
|
Filter,
|
|
@@ -62,6 +61,8 @@ import { deriveProvenance, hasConnectionId, readUiCacheFlag } from "@/lib/worksp
|
|
|
62
61
|
import { ApiRegistryCreationCockpit } from "../data-model/components/ApiRegistryCreationCockpit.jsx";
|
|
63
62
|
import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
|
|
64
63
|
import { deriveServerlessUpgradeState, SERVERLESS_UPGRADE_DISMISS_FLAG } from "@/lib/serverless-upgrade";
|
|
64
|
+
import { UPSTASH_QSTASH_INTEGRATION_ID, deriveWorkspaceAddOnsState } from "@/lib/workspace-add-ons";
|
|
65
|
+
import { scanServerlessReadiness, readinessFieldFlags } from "@/lib/serverless-readiness";
|
|
65
66
|
|
|
66
67
|
// Set a flag on the governed workspace-ui-cache "activation" row (pure helper,
|
|
67
68
|
// same transform the rail/lens one-time dismisses use).
|
|
@@ -136,6 +137,20 @@ function resolveRegistryRowForSandbox(workspaceConfig, sandboxRow) {
|
|
|
136
137
|
return resolveRegistryRefForSandbox(workspaceConfig, sandboxRow)?.row || null;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
function resolveSchedulerRegistryRows(workspaceConfig) {
|
|
141
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
142
|
+
const rows = [];
|
|
143
|
+
for (const objectItem of objects) {
|
|
144
|
+
if (objectItem?.objectType !== "api-registry") continue;
|
|
145
|
+
for (const row of Array.isArray(objectItem.rows) ? objectItem.rows : []) {
|
|
146
|
+
if (String(row?.executionLane || "").trim() !== "serverless-scheduler") continue;
|
|
147
|
+
const integrationId = String(row?.integrationId || "").trim();
|
|
148
|
+
if (integrationId) rows.push({ object: objectItem, row, integrationId });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
|
|
140
155
|
const graph = parseOrchestrationGraph(sandboxRow?.orchestrationConfig || sandboxRow?.orchestrationGraph);
|
|
141
156
|
const apiNode = graph?.nodes?.find((n) => n?.type === "api-registry-call");
|
|
@@ -162,6 +177,235 @@ function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
|
|
|
162
177
|
return firstRegistryRow ? { object: firstRegistryObject, row: firstRegistryRow } : null;
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
function WorkflowAddOnChooser({ addOn, disabled, onUseQstash, onSetupQstash, onSetupCustom }) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="dm-workflow-addon-choice-list">
|
|
183
|
+
<section className="dm-api-action-card dm-workflow-installed-addon">
|
|
184
|
+
<div className="dm-api-action-card-body">
|
|
185
|
+
<p className="dm-api-action-card-eyebrow">{addOn ? "Verified workflow add-on" : "Workflow add-on"}</p>
|
|
186
|
+
<h3>Upstash QStash/Workflow</h3>
|
|
187
|
+
<p>
|
|
188
|
+
{addOn
|
|
189
|
+
? "Bind the verified workspace QStash scheduler to this workflow and switch it to serverless."
|
|
190
|
+
: "Install and sync QStash in Workspace Add-ons first; the canvas only binds verified scheduler rows."}
|
|
191
|
+
</p>
|
|
192
|
+
<div className="dm-cockpit-fields">
|
|
193
|
+
<span className="dm-cockpit-field"><b>registry</b>{addOn?.integrationId || UPSTASH_QSTASH_INTEGRATION_ID}</span>
|
|
194
|
+
<span className="dm-cockpit-field"><b>status</b>{addOn?.syncStatus || "setup required"}</span>
|
|
195
|
+
<span className="dm-cockpit-field"><b>region</b>{addOn?.region || "pending"}</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="dm-api-action-card-actions">
|
|
199
|
+
{addOn ? (
|
|
200
|
+
<button type="button" className="dm-btn-primary-sm dm-api-action-card-cta" disabled={disabled} onClick={onUseQstash}>
|
|
201
|
+
Use for this workflow
|
|
202
|
+
</button>
|
|
203
|
+
) : (
|
|
204
|
+
<button type="button" className="dm-btn-outline dm-api-action-card-cta" disabled={disabled} onClick={onSetupQstash}>
|
|
205
|
+
Set up QStash
|
|
206
|
+
</button>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
<section className="dm-api-action-card dm-workflow-installed-addon">
|
|
211
|
+
<div className="dm-api-action-card-body">
|
|
212
|
+
<p className="dm-api-action-card-eyebrow">Custom</p>
|
|
213
|
+
<h3>Custom scheduler plugin</h3>
|
|
214
|
+
<p>Use a governed API Registry scheduler row and bind it through this sandbox row's schedulerRegistryId field.</p>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="dm-api-action-card-actions">
|
|
217
|
+
<button type="button" className="dm-btn-outline dm-api-action-card-cta" disabled={disabled} onClick={onSetupCustom}>
|
|
218
|
+
Configure custom
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</section>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const SCHEDULE_CADENCE_OPTIONS = [
|
|
227
|
+
{ id: "daily", label: "Daily" },
|
|
228
|
+
{ id: "weekly", label: "Weekly" },
|
|
229
|
+
{ id: "monthly", label: "Monthly" },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const SCHEDULE_WEEKDAY_OPTIONS = [
|
|
233
|
+
{ id: "1", label: "Monday" },
|
|
234
|
+
{ id: "2", label: "Tuesday" },
|
|
235
|
+
{ id: "3", label: "Wednesday" },
|
|
236
|
+
{ id: "4", label: "Thursday" },
|
|
237
|
+
{ id: "5", label: "Friday" },
|
|
238
|
+
{ id: "6", label: "Saturday" },
|
|
239
|
+
{ id: "0", label: "Sunday" },
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const SCHEDULE_HOUR_OPTIONS = Array.from({ length: 24 }, (_, hour) => {
|
|
243
|
+
const id = String(hour).padStart(2, "0");
|
|
244
|
+
return { id, label: id };
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const SCHEDULE_MINUTE_OPTIONS = Array.from({ length: 12 }, (_, index) => {
|
|
248
|
+
const minute = String(index * 5).padStart(2, "0");
|
|
249
|
+
return { id: minute, label: minute };
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const SCHEDULE_MONTH_DAY_OPTIONS = Array.from({ length: 28 }, (_, index) => {
|
|
253
|
+
const day = String(index + 1);
|
|
254
|
+
return { id: day, label: day };
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
function splitScheduleTime(value) {
|
|
258
|
+
const [rawHour, rawMinute] = String(value || "09:00").split(":");
|
|
259
|
+
const hour = Math.min(23, Math.max(0, Number(rawHour) || 0));
|
|
260
|
+
const minute = Math.min(59, Math.max(0, Number(rawMinute) || 0));
|
|
261
|
+
return {
|
|
262
|
+
hour: String(hour).padStart(2, "0"),
|
|
263
|
+
minute: String(Math.round(minute / 5) * 5).padStart(2, "0"),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function joinScheduleTime(hour, minute) {
|
|
268
|
+
const safeHour = Math.min(23, Math.max(0, Number(hour) || 0));
|
|
269
|
+
const safeMinute = Math.min(59, Math.max(0, Number(minute) || 0));
|
|
270
|
+
return `${String(safeHour).padStart(2, "0")}:${String(safeMinute).padStart(2, "0")}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function ScheduleTimeControls({ value, onChange }) {
|
|
274
|
+
const { hour, minute } = splitScheduleTime(value);
|
|
275
|
+
return (
|
|
276
|
+
<div className="dm-workflow-schedule-time-grid">
|
|
277
|
+
<label className="dm-orchestration-config__field">
|
|
278
|
+
<span>Hour (UTC)</span>
|
|
279
|
+
<select value={hour} onChange={(event) => onChange(joinScheduleTime(event.target.value, minute))}>
|
|
280
|
+
{SCHEDULE_HOUR_OPTIONS.map((option) => (
|
|
281
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
282
|
+
))}
|
|
283
|
+
</select>
|
|
284
|
+
</label>
|
|
285
|
+
<label className="dm-orchestration-config__field">
|
|
286
|
+
<span>Minute</span>
|
|
287
|
+
<select value={minute} onChange={(event) => onChange(joinScheduleTime(hour, event.target.value))}>
|
|
288
|
+
{SCHEDULE_MINUTE_OPTIONS.map((option) => (
|
|
289
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
290
|
+
))}
|
|
291
|
+
</select>
|
|
292
|
+
</label>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function deriveCronFromSchedule({ cadence, time, weekday, monthDay }) {
|
|
298
|
+
const [hourText, minuteText] = String(time || "09:00").split(":");
|
|
299
|
+
const hour = Math.min(23, Math.max(0, Number(hourText) || 0));
|
|
300
|
+
const minute = Math.min(59, Math.max(0, Number(minuteText) || 0));
|
|
301
|
+
if (cadence === "weekly") return `${minute} ${hour} * * ${String(weekday || "1")}`;
|
|
302
|
+
if (cadence === "monthly") return `${minute} ${hour} ${Math.min(28, Math.max(1, Number(monthDay) || 1))} * *`;
|
|
303
|
+
return `${minute} ${hour} * * *`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function describeSchedule({ cadence, time, weekday, monthDay }) {
|
|
307
|
+
const selectedWeekday = SCHEDULE_WEEKDAY_OPTIONS.find((option) => option.id === String(weekday))?.label || "Monday";
|
|
308
|
+
if (cadence === "weekly") return `Runs every ${selectedWeekday} at ${time || "09:00"} UTC.`;
|
|
309
|
+
if (cadence === "monthly") return `Runs on day ${Math.min(28, Math.max(1, Number(monthDay) || 1))} of each month at ${time || "09:00"} UTC.`;
|
|
310
|
+
return `Runs every day at ${time || "09:00"} UTC.`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildServerlessScheduleRunInputs({ workflow, cadence, time, weekday, monthDay }) {
|
|
314
|
+
return {
|
|
315
|
+
kind: "growthub-workflow-run-inputs-v1",
|
|
316
|
+
source: "serverless-scheduler",
|
|
317
|
+
values: {
|
|
318
|
+
trigger: "scheduled",
|
|
319
|
+
source: "serverless-scheduler",
|
|
320
|
+
workflow: String(workflow || ""),
|
|
321
|
+
cadence: String(cadence || ""),
|
|
322
|
+
runTimeUtc: String(time || ""),
|
|
323
|
+
weekday: String(weekday || ""),
|
|
324
|
+
monthDay: String(monthDay || "")
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function WorkflowScheduleModal({
|
|
330
|
+
open,
|
|
331
|
+
addOn,
|
|
332
|
+
workflowName,
|
|
333
|
+
cadence,
|
|
334
|
+
scheduleTime,
|
|
335
|
+
scheduleWeekday,
|
|
336
|
+
scheduleMonthDay,
|
|
337
|
+
errorMessage,
|
|
338
|
+
disabled,
|
|
339
|
+
onCadenceChange,
|
|
340
|
+
onScheduleTimeChange,
|
|
341
|
+
onScheduleWeekdayChange,
|
|
342
|
+
onScheduleMonthDayChange,
|
|
343
|
+
onSubmit,
|
|
344
|
+
onClose,
|
|
345
|
+
}) {
|
|
346
|
+
if (!open) return null;
|
|
347
|
+
return (
|
|
348
|
+
<div className="dm-workflow-schedule-backdrop" role="presentation">
|
|
349
|
+
<section className="dm-workflow-schedule-modal" role="dialog" aria-modal="true" aria-label="Configure QStash schedule">
|
|
350
|
+
<header>
|
|
351
|
+
<div>
|
|
352
|
+
<p className="dm-api-action-card-eyebrow">Serverless schedule</p>
|
|
353
|
+
<h3>Use QStash for {workflowName || "this workflow"}</h3>
|
|
354
|
+
</div>
|
|
355
|
+
<button type="button" className="dm-workflow-icon-btn" aria-label="Close schedule modal" onClick={onClose}>
|
|
356
|
+
<X size={14} />
|
|
357
|
+
</button>
|
|
358
|
+
</header>
|
|
359
|
+
<div className="dm-workflow-schedule-body">
|
|
360
|
+
<div className="dm-marketplace-config-summary">
|
|
361
|
+
<div><span>Registry</span><code>{addOn?.integrationId || UPSTASH_QSTASH_INTEGRATION_ID}</code></div>
|
|
362
|
+
<div><span>Region</span><code>{addOn?.region || "pending"}</code></div>
|
|
363
|
+
<div><span>Status</span><code>{addOn?.syncStatus || "setup required"}</code></div>
|
|
364
|
+
</div>
|
|
365
|
+
<label className="dm-marketplace-field">
|
|
366
|
+
<span>Cadence</span>
|
|
367
|
+
<select value={cadence} onChange={(event) => onCadenceChange(event.target.value)}>
|
|
368
|
+
{SCHEDULE_CADENCE_OPTIONS.map((option) => (
|
|
369
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
370
|
+
))}
|
|
371
|
+
</select>
|
|
372
|
+
</label>
|
|
373
|
+
<ScheduleTimeControls value={scheduleTime} onChange={onScheduleTimeChange} />
|
|
374
|
+
{cadence === "weekly" ? (
|
|
375
|
+
<label className="dm-marketplace-field">
|
|
376
|
+
<span>Run day</span>
|
|
377
|
+
<select value={scheduleWeekday} onChange={(event) => onScheduleWeekdayChange(event.target.value)}>
|
|
378
|
+
{SCHEDULE_WEEKDAY_OPTIONS.map((option) => (
|
|
379
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
380
|
+
))}
|
|
381
|
+
</select>
|
|
382
|
+
</label>
|
|
383
|
+
) : null}
|
|
384
|
+
{cadence === "monthly" ? (
|
|
385
|
+
<label className="dm-marketplace-field">
|
|
386
|
+
<span>Day of month</span>
|
|
387
|
+
<select value={scheduleMonthDay} onChange={(event) => onScheduleMonthDayChange(event.target.value)}>
|
|
388
|
+
{SCHEDULE_MONTH_DAY_OPTIONS.map((option) => (
|
|
389
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
390
|
+
))}
|
|
391
|
+
</select>
|
|
392
|
+
</label>
|
|
393
|
+
) : null}
|
|
394
|
+
<p className="dm-cockpit-step-hint">{describeSchedule({ cadence, time: scheduleTime, weekday: scheduleWeekday, monthDay: scheduleMonthDay })}</p>
|
|
395
|
+
{errorMessage ? <p className="dm-workflow-schedule-error" role="alert">{errorMessage}</p> : null}
|
|
396
|
+
<p className="dm-cockpit-step-hint">This writes the sandbox row to serverless, stores the cadence and trigger input on the workflow trigger, and submits the QStash schedule through the server-owned workspace origin.</p>
|
|
397
|
+
</div>
|
|
398
|
+
<footer>
|
|
399
|
+
<button type="button" className="dm-btn-outline" onClick={onClose}>Cancel</button>
|
|
400
|
+
<button type="button" className="dm-btn-primary-sm" disabled={disabled || !String(scheduleTime || "").trim()} onClick={onSubmit}>
|
|
401
|
+
{disabled ? "Scheduling..." : "Create schedule"}
|
|
402
|
+
</button>
|
|
403
|
+
</footer>
|
|
404
|
+
</section>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
165
409
|
const WORKFLOW_ACTION_GROUPS = [
|
|
166
410
|
{
|
|
167
411
|
label: "Data",
|
|
@@ -192,7 +436,6 @@ const WORKFLOW_ACTION_GROUPS = [
|
|
|
192
436
|
{ id: "http-request", label: "HTTP Request", type: "core-action", Icon: Globe2, destructive: false },
|
|
193
437
|
],
|
|
194
438
|
},
|
|
195
|
-
{ label: "Human Input", items: [{ id: "form", label: "Form", type: "human-input", Icon: FormInput, destructive: false }] },
|
|
196
439
|
];
|
|
197
440
|
|
|
198
441
|
function getWorkspaceObjectOptions(workspaceConfig) {
|
|
@@ -214,7 +457,7 @@ function makeWorkflowNode(action, workspaceConfig, graph) {
|
|
|
214
457
|
id = `${baseId}-${index}`;
|
|
215
458
|
index += 1;
|
|
216
459
|
}
|
|
217
|
-
const isData = action.type === "data-action"
|
|
460
|
+
const isData = action.type === "data-action";
|
|
218
461
|
return {
|
|
219
462
|
id,
|
|
220
463
|
type: action.type,
|
|
@@ -333,6 +576,13 @@ export default function WorkflowSurface() {
|
|
|
333
576
|
const [dirty, setDirty] = useState(false);
|
|
334
577
|
const [runSetupOpen, setRunSetupOpen] = useState(false);
|
|
335
578
|
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
|
579
|
+
const [scheduleModalOpen, setScheduleModalOpen] = useState(false);
|
|
580
|
+
const [scheduleCadence, setScheduleCadence] = useState("daily");
|
|
581
|
+
const [scheduleTime, setScheduleTime] = useState("09:00");
|
|
582
|
+
const [scheduleWeekday, setScheduleWeekday] = useState("1");
|
|
583
|
+
const [scheduleMonthDay, setScheduleMonthDay] = useState("1");
|
|
584
|
+
const [scheduleError, setScheduleError] = useState("");
|
|
585
|
+
const [remoteScheduleState, setRemoteScheduleState] = useState({ status: "idle", verified: false, scheduleId: "", proof: "" });
|
|
336
586
|
const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
|
|
337
587
|
|
|
338
588
|
useEffect(() => {
|
|
@@ -380,6 +630,43 @@ export default function WorkflowSurface() {
|
|
|
380
630
|
|
|
381
631
|
const sandboxRow = resolved.row;
|
|
382
632
|
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
const scheduleId = String(sandboxRow?.scheduleId || "").trim();
|
|
635
|
+
const rowScheduler = String(sandboxRow?.schedulerRegistryId || "").trim();
|
|
636
|
+
const rowLocality = String(sandboxRow?.runLocality || "").trim();
|
|
637
|
+
if (!objectId || !rowId || rowLocality !== "serverless" || !scheduleId || rowScheduler !== UPSTASH_QSTASH_INTEGRATION_ID) {
|
|
638
|
+
setRemoteScheduleState({ status: "idle", verified: false, scheduleId: "", proof: "" });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
let cancelled = false;
|
|
642
|
+
const params = new URLSearchParams({
|
|
643
|
+
productId: "upstash-qstash",
|
|
644
|
+
objectId,
|
|
645
|
+
rowId,
|
|
646
|
+
scheduleId,
|
|
647
|
+
region: String(sandboxRow?.schedulerRegion || "us-east-1").trim(),
|
|
648
|
+
});
|
|
649
|
+
setRemoteScheduleState({ status: "checking", verified: false, scheduleId, proof: "" });
|
|
650
|
+
fetch(`/api/workspace/add-ons/upstash/schedule?${params.toString()}`, { cache: "no-store" })
|
|
651
|
+
.then(async (res) => {
|
|
652
|
+
const payload = await res.json().catch(() => ({}));
|
|
653
|
+
if (cancelled) return;
|
|
654
|
+
setRemoteScheduleState({
|
|
655
|
+
status: res.ok && payload.verified ? "verified" : "missing",
|
|
656
|
+
verified: res.ok && payload.verified === true && String(payload.remoteScheduleId || payload.scheduleId || "").trim() === scheduleId,
|
|
657
|
+
scheduleId,
|
|
658
|
+
proof: payload.proof || payload.error || "",
|
|
659
|
+
});
|
|
660
|
+
})
|
|
661
|
+
.catch((error) => {
|
|
662
|
+
if (cancelled) return;
|
|
663
|
+
setRemoteScheduleState({ status: "error", verified: false, scheduleId, proof: error?.message || "remote schedule verification failed" });
|
|
664
|
+
});
|
|
665
|
+
return () => { cancelled = true; };
|
|
666
|
+
}, [objectId, rowId, sandboxRow?.runLocality, sandboxRow?.schedulerRegistryId, sandboxRow?.scheduleId, sandboxRow?.schedulerRegion]);
|
|
667
|
+
|
|
668
|
+
const remoteScheduleVerified = remoteScheduleState.verified === true;
|
|
669
|
+
|
|
383
670
|
// Per-node Workflow Canvas pill status — GENERAL orchestration (not swarm).
|
|
384
671
|
// Live from the streamed orchestration.node.* deltas while a run is in
|
|
385
672
|
// flight; settled from the persisted run record's nodeTrace once complete.
|
|
@@ -461,10 +748,11 @@ export default function WorkflowSurface() {
|
|
|
461
748
|
if (!orchestrationGraph?.nodes || !selectedNodeId) return null;
|
|
462
749
|
const node = orchestrationGraph.nodes.find((n) => String(n.id) === selectedNodeId) || null;
|
|
463
750
|
if (!node) return null;
|
|
751
|
+
const baseConfig = node.config || {};
|
|
464
752
|
return {
|
|
465
753
|
...node,
|
|
466
754
|
config: {
|
|
467
|
-
...
|
|
755
|
+
...baseConfig,
|
|
468
756
|
sandboxRecordRef: nodeSandboxRecordRef(objectId, rowId, node.id)
|
|
469
757
|
}
|
|
470
758
|
};
|
|
@@ -667,6 +955,82 @@ export default function WorkflowSurface() {
|
|
|
667
955
|
}
|
|
668
956
|
}
|
|
669
957
|
|
|
958
|
+
function findWorkflowRowInConfig(config) {
|
|
959
|
+
return findSandboxRowByWorkflowRef(config, objectId, rowId)?.row || null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function fetchWorkspaceConfigOnce() {
|
|
963
|
+
const res = await fetch("/api/workspace", { cache: "no-store" });
|
|
964
|
+
const payload = await res.json().catch(() => ({}));
|
|
965
|
+
if (!res.ok) throw new Error(payload.error || "Failed to load workspace");
|
|
966
|
+
if (payload.workspaceConfig) setWorkspaceConfig(payload.workspaceConfig);
|
|
967
|
+
return payload.workspaceConfig || null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function waitForScheduledRunProof(messageId) {
|
|
971
|
+
const wanted = String(messageId || "").trim();
|
|
972
|
+
const maxAttempts = 20;
|
|
973
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
974
|
+
const latestConfig = await fetchWorkspaceConfigOnce();
|
|
975
|
+
const latestRow = findWorkflowRowInConfig(latestConfig);
|
|
976
|
+
const latestMessageId = String(latestRow?.lastScheduledRunMessageId || "").trim();
|
|
977
|
+
const latestStatus = String(latestRow?.lastScheduledRunStatus || "").trim();
|
|
978
|
+
if ((wanted && latestMessageId === wanted) || (!wanted && latestStatus)) return latestRow;
|
|
979
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
980
|
+
}
|
|
981
|
+
return findWorkflowRowInConfig(workspaceConfig);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function runInstalledSchedulerNow() {
|
|
985
|
+
if (!objectId || !rowId || !workspaceConfig || !sandboxRow?.scheduleId || !remoteScheduleVerified) return runSandbox();
|
|
986
|
+
setRunning(true);
|
|
987
|
+
setRunMessage("");
|
|
988
|
+
setLiveRunEvents([]);
|
|
989
|
+
try {
|
|
990
|
+
const triggerInput = JSON.stringify(buildServerlessScheduleRunInputs({
|
|
991
|
+
workflow: rowId,
|
|
992
|
+
cadence: scheduleCadence,
|
|
993
|
+
time: scheduleTime,
|
|
994
|
+
weekday: scheduleWeekday,
|
|
995
|
+
monthDay: scheduleMonthDay,
|
|
996
|
+
}));
|
|
997
|
+
const response = await fetch("/api/workspace/add-ons/upstash/schedule", {
|
|
998
|
+
method: "POST",
|
|
999
|
+
headers: { "content-type": "application/json" },
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
action: "run",
|
|
1002
|
+
productId: "upstash-qstash",
|
|
1003
|
+
objectId,
|
|
1004
|
+
rowId,
|
|
1005
|
+
region: sandboxRow.schedulerRegion || addOnsState.qstashWorkflow?.region || "us-east-1",
|
|
1006
|
+
scheduleId: sandboxRow.scheduleId,
|
|
1007
|
+
version: String(sandboxRow?.version || "v1"),
|
|
1008
|
+
workspaceId: workspaceConfig?.id || "workspace",
|
|
1009
|
+
triggerInput,
|
|
1010
|
+
}),
|
|
1011
|
+
});
|
|
1012
|
+
const payload = await response.json().catch(() => ({}));
|
|
1013
|
+
if (!response.ok || !payload.ok) {
|
|
1014
|
+
throw new Error(payload.error || "Scheduler run could not be published.");
|
|
1015
|
+
}
|
|
1016
|
+
setRunMessage(payload.messageId ? `Scheduler run published (${payload.messageId}). Waiting for callback proof...` : "Scheduler run published. Waiting for callback proof...");
|
|
1017
|
+
const proofRow = await waitForScheduledRunProof(payload.messageId);
|
|
1018
|
+
const status = String(proofRow?.lastScheduledRunStatus || "").trim();
|
|
1019
|
+
const succeeded = status && Number(status) >= 200 && Number(status) < 300;
|
|
1020
|
+
if (succeeded) {
|
|
1021
|
+
setRunMessage(`Scheduler run succeeded with HTTP ${status}.`);
|
|
1022
|
+
} else if (status) {
|
|
1023
|
+
setRunMessage(redactSecretsFromText(`Scheduler run returned HTTP ${status}: ${proofRow?.lastScheduledRunFailureReason || proofRow?.lastScheduledRunBodyPreview || "see run proof"}`));
|
|
1024
|
+
} else {
|
|
1025
|
+
setRunMessage("Scheduler run was published, but callback proof did not land before the UI timeout.");
|
|
1026
|
+
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
setRunMessage(redactSecretsFromText(err.message || "Scheduler run failed"));
|
|
1029
|
+
} finally {
|
|
1030
|
+
setRunning(false);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
670
1034
|
function openTraceMode() {
|
|
671
1035
|
const params = new URLSearchParams(searchParams.toString());
|
|
672
1036
|
params.delete("run");
|
|
@@ -807,7 +1171,29 @@ export default function WorkflowSurface() {
|
|
|
807
1171
|
const orderedNodes = orchestrationGraph?.nodes || [];
|
|
808
1172
|
const currentGraphSerialized = graphUnset ? "" : serializeOrchestrationGraph(orchestrationGraph);
|
|
809
1173
|
const draftPassed = sandboxRow?.orchestrationDraftTestPassed === true || String(sandboxRow?.orchestrationDraftTestPassed || "") === "true";
|
|
810
|
-
const
|
|
1174
|
+
const liveScheduleBinding = (() => {
|
|
1175
|
+
const graph = parseOrchestrationGraph(sandboxRow?.orchestrationGraph || sandboxRow?.orchestrationConfig);
|
|
1176
|
+
const node = (Array.isArray(graph?.nodes) ? graph.nodes : []).find((entry) => entry?.type === "input" || entry?.id === "input");
|
|
1177
|
+
const schedule = node?.config?.schedule && typeof node.config.schedule === "object" ? node.config.schedule : {};
|
|
1178
|
+
return {
|
|
1179
|
+
enabled: node?.config?.enabled !== false,
|
|
1180
|
+
scheduleId: String(schedule.scheduleId || "").trim(),
|
|
1181
|
+
schedulerRegistryId: String(schedule.schedulerRegistryId || "").trim()
|
|
1182
|
+
};
|
|
1183
|
+
})();
|
|
1184
|
+
const serverlessInstalledAndBound =
|
|
1185
|
+
String(sandboxRow?.runLocality || "").trim() === "serverless" &&
|
|
1186
|
+
Boolean(String(sandboxRow?.scheduleId || "").trim()) &&
|
|
1187
|
+
remoteScheduleVerified &&
|
|
1188
|
+
Boolean(String(sandboxRow?.schedulerRegistryId || "").trim()) &&
|
|
1189
|
+
liveScheduleBinding.enabled === true &&
|
|
1190
|
+
liveScheduleBinding.scheduleId === String(sandboxRow?.scheduleId || "").trim() &&
|
|
1191
|
+
liveScheduleBinding.schedulerRegistryId === String(sandboxRow?.schedulerRegistryId || "").trim() &&
|
|
1192
|
+
String(sandboxRow?.orchestrationDraftConfig || sandboxRow?.orchestrationDraftGraph || "").trim() === currentGraphSerialized;
|
|
1193
|
+
const publishReady = !dirty && (
|
|
1194
|
+
(draftPassed && String(sandboxRow?.orchestrationDraftTestedConfig || "") === currentGraphSerialized) ||
|
|
1195
|
+
serverlessInstalledAndBound
|
|
1196
|
+
);
|
|
811
1197
|
const savedDraftValue = String(sandboxRow?.[draftFieldName] || "").trim();
|
|
812
1198
|
const draftStatus = String(sandboxRow?.orchestrationDraftStatus || "").trim();
|
|
813
1199
|
const hasSavedDraft = Boolean(savedDraftValue) && draftStatus !== "published" && graphHasNodes(parseOrchestrationGraph(savedDraftValue));
|
|
@@ -830,7 +1216,32 @@ export default function WorkflowSurface() {
|
|
|
830
1216
|
persistenceAdapters: serverlessSignals.persistenceAdapters,
|
|
831
1217
|
})
|
|
832
1218
|
: null;
|
|
1219
|
+
const addOnsState = deriveWorkspaceAddOnsState(workspaceConfig || {});
|
|
1220
|
+
const schedulerRegistryRows = resolveSchedulerRegistryRows(workspaceConfig || {});
|
|
1221
|
+
const selectedSchedulerRegistryId = String(sandboxRow?.schedulerRegistryId || addOnsState.qstashWorkflow?.integrationId || schedulerRegistryRows[0]?.integrationId || "").trim();
|
|
1222
|
+
const selectedSchedulerRow = schedulerRegistryRows.find((entry) => entry.integrationId === selectedSchedulerRegistryId)?.row || addOnsState.qstashWorkflow || null;
|
|
833
1223
|
const isServerlessWorkflow = Boolean(serverlessState?.isServerless);
|
|
1224
|
+
|
|
1225
|
+
// Serverless-readiness — a PURE causation driver, the same shape/inputs as
|
|
1226
|
+
// deriveSandboxServerlessState above (no fetch, no effect): the credential
|
|
1227
|
+
// signal is the already-resolved `serverlessSignals.configuredEnvRefs` (slugs,
|
|
1228
|
+
// never values). It runs once the input trigger is in (or moving into)
|
|
1229
|
+
// Serverless Schedule, and feeds the ultrathin orange node border + the
|
|
1230
|
+
// light-orange field/delta-tag fills (the color is the only guidance added).
|
|
1231
|
+
const inputServerlessSelected = String(
|
|
1232
|
+
(Array.isArray(orchestrationGraph?.nodes) ? orchestrationGraph.nodes : [])
|
|
1233
|
+
.find((n) => n?.type === "input" || n?.id === "input" || n?.type === "data-trigger")?.config?.inputMode || "",
|
|
1234
|
+
).trim() === "serverless-schedule";
|
|
1235
|
+
const serverlessReadiness = sandboxRow && (isServerlessWorkflow || inputServerlessSelected)
|
|
1236
|
+
? scanServerlessReadiness({
|
|
1237
|
+
row: sandboxRow,
|
|
1238
|
+
workspaceConfig: workspaceConfig || {},
|
|
1239
|
+
configuredEnvRefs: serverlessSignals.configuredEnvRefs,
|
|
1240
|
+
phase: isServerlessWorkflow && String(sandboxRow?.scheduleId || "").trim() ? "bound" : "pre-bind",
|
|
1241
|
+
expected: { schedulerRegistryId: selectedSchedulerRegistryId, scheduleId: String(sandboxRow?.scheduleId || "").trim() },
|
|
1242
|
+
})
|
|
1243
|
+
: null;
|
|
1244
|
+
const readinessFlags = serverlessReadiness ? readinessFieldFlags(serverlessReadiness) : {};
|
|
834
1245
|
const showServerlessUpgrade = String(sandboxRow?.adapter || "").trim() !== "local-intelligence";
|
|
835
1246
|
|
|
836
1247
|
async function patchSandboxAndPersist(fields) {
|
|
@@ -855,13 +1266,7 @@ export default function WorkflowSurface() {
|
|
|
855
1266
|
await patchSandboxAndPersist({ runLocality: "local" });
|
|
856
1267
|
return;
|
|
857
1268
|
}
|
|
858
|
-
|
|
859
|
-
const adapterId = String(sandboxRow?.adapter || "").trim();
|
|
860
|
-
await patchSandboxAndPersist({
|
|
861
|
-
runLocality: "serverless",
|
|
862
|
-
schedulerRegistryId: String(registryRow?.integrationId || "").trim(),
|
|
863
|
-
adapter: ["local-agent-host", "local-intelligence"].includes(adapterId) ? "local-process" : adapterId,
|
|
864
|
-
});
|
|
1269
|
+
setUpgradeOpen(true);
|
|
865
1270
|
} else if (action.id === "open-settings") {
|
|
866
1271
|
router.push(action.href || "/settings");
|
|
867
1272
|
} else if (action.id === "link-scheduler") {
|
|
@@ -874,6 +1279,175 @@ export default function WorkflowSurface() {
|
|
|
874
1279
|
} else if (action.id === "edit-adapter") {
|
|
875
1280
|
// Full scheduler/adapter config lives on the sandbox object's drawer.
|
|
876
1281
|
router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}`);
|
|
1282
|
+
} else if (action.id === "run-sandbox") {
|
|
1283
|
+
if (
|
|
1284
|
+
isServerlessWorkflow &&
|
|
1285
|
+
sandboxRow?.schedulerRegistryId === UPSTASH_QSTASH_INTEGRATION_ID &&
|
|
1286
|
+
sandboxRow?.scheduleId &&
|
|
1287
|
+
remoteScheduleVerified
|
|
1288
|
+
) {
|
|
1289
|
+
await runInstalledSchedulerNow();
|
|
1290
|
+
} else {
|
|
1291
|
+
await runSandbox();
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async function useInstalledQstashWorkflowAddOn() {
|
|
1297
|
+
setScheduleModalOpen(true);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function updateScheduleCadence(cadence) {
|
|
1301
|
+
const next = SCHEDULE_CADENCE_OPTIONS.find((option) => option.id === cadence) || SCHEDULE_CADENCE_OPTIONS[0];
|
|
1302
|
+
setScheduleCadence(next.id);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function submitQstashSchedule() {
|
|
1306
|
+
// Bind requires an installed+verified QStash product. The schedule route
|
|
1307
|
+
// installs THIS row's schedule AND flips it to serverless in ONE
|
|
1308
|
+
// server-authoritative write, then returns the persisted config — we adopt
|
|
1309
|
+
// it verbatim. No second PATCH over stale state (which could clobber the
|
|
1310
|
+
// just-written scheduleId), and serverless is never claimed unless the
|
|
1311
|
+
// server confirmed both the remote schedule and the local persist.
|
|
1312
|
+
if (resolved.rowIndex < 0 || !objectId || !rowId || !workspaceConfig || !addOnsState.qstashWorkflow) return false;
|
|
1313
|
+
if (!String(scheduleTime || "").trim()) {
|
|
1314
|
+
setScheduleError("Choose a run time.");
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
const scheduleCron = deriveCronFromSchedule({
|
|
1318
|
+
cadence: scheduleCadence,
|
|
1319
|
+
time: scheduleTime,
|
|
1320
|
+
weekday: scheduleWeekday,
|
|
1321
|
+
monthDay: scheduleMonthDay,
|
|
1322
|
+
});
|
|
1323
|
+
const triggerInput = JSON.stringify(buildServerlessScheduleRunInputs({
|
|
1324
|
+
workflow: rowId,
|
|
1325
|
+
cadence: scheduleCadence,
|
|
1326
|
+
time: scheduleTime,
|
|
1327
|
+
weekday: scheduleWeekday,
|
|
1328
|
+
monthDay: scheduleMonthDay,
|
|
1329
|
+
}));
|
|
1330
|
+
setScheduleError("");
|
|
1331
|
+
setSaving(true);
|
|
1332
|
+
setSaveMessage("");
|
|
1333
|
+
try {
|
|
1334
|
+
const response = await fetch("/api/workspace/add-ons/upstash/schedule", {
|
|
1335
|
+
method: "POST",
|
|
1336
|
+
headers: { "content-type": "application/json" },
|
|
1337
|
+
body: JSON.stringify({
|
|
1338
|
+
productId: "upstash-qstash",
|
|
1339
|
+
objectId,
|
|
1340
|
+
rowId,
|
|
1341
|
+
region: addOnsState.qstashWorkflow.region || "us-east-1",
|
|
1342
|
+
cron: scheduleCron,
|
|
1343
|
+
cadence: scheduleCadence,
|
|
1344
|
+
triggerInput,
|
|
1345
|
+
version: String(sandboxRow?.version || "v1"),
|
|
1346
|
+
workspaceId: workspaceConfig?.id || "workspace",
|
|
1347
|
+
}),
|
|
1348
|
+
});
|
|
1349
|
+
const payload = await response.json().catch(() => ({}));
|
|
1350
|
+
if (!response.ok || !payload.bound || !payload.workspaceConfig) {
|
|
1351
|
+
// No schedule installed (missing token / read-only / provider / persist
|
|
1352
|
+
// failure). Keep the workflow local; route the operator to finish setup.
|
|
1353
|
+
setScheduleError(payload.error ? `Could not create schedule: ${payload.error}` : "Could not create the QStash schedule.");
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
setWorkspaceConfig(payload.workspaceConfig);
|
|
1357
|
+
setScheduleModalOpen(false);
|
|
1358
|
+
setSaveMessage("Schedule updated.");
|
|
1359
|
+
return true;
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
console.warn(error);
|
|
1362
|
+
setSaveMessage(error?.message || "Could not create QStash schedule.");
|
|
1363
|
+
return false;
|
|
1364
|
+
} finally {
|
|
1365
|
+
setSaving(false);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function revertServerlessScheduleToLocal() {
|
|
1370
|
+
if (!objectId || !rowId || !sandboxRow?.scheduleId) return false;
|
|
1371
|
+
setScheduleError("");
|
|
1372
|
+
setSaving(true);
|
|
1373
|
+
setSaveMessage("");
|
|
1374
|
+
try {
|
|
1375
|
+
const response = await fetch("/api/workspace/add-ons/upstash/schedule", {
|
|
1376
|
+
method: "DELETE",
|
|
1377
|
+
headers: { "content-type": "application/json" },
|
|
1378
|
+
body: JSON.stringify({
|
|
1379
|
+
productId: "upstash-qstash",
|
|
1380
|
+
objectId,
|
|
1381
|
+
rowId,
|
|
1382
|
+
scheduleId: sandboxRow.scheduleId,
|
|
1383
|
+
}),
|
|
1384
|
+
});
|
|
1385
|
+
const payload = await response.json().catch(() => ({}));
|
|
1386
|
+
if (!response.ok || !payload.ok || !payload.workspaceConfig) {
|
|
1387
|
+
setScheduleError(payload.error || "Could not remove the remote schedule.");
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
setWorkspaceConfig(payload.workspaceConfig);
|
|
1391
|
+
setSaveMessage("Schedule removed.");
|
|
1392
|
+
return true;
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
setScheduleError(error?.message || "Could not remove the remote schedule.");
|
|
1395
|
+
return false;
|
|
1396
|
+
} finally {
|
|
1397
|
+
setSaving(false);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function controlInstalledScheduler(action) {
|
|
1402
|
+
if (!objectId || !rowId || !sandboxRow?.scheduleId || !["pause", "resume"].includes(action)) return false;
|
|
1403
|
+
setScheduleError("");
|
|
1404
|
+
setSaving(true);
|
|
1405
|
+
setSaveMessage("");
|
|
1406
|
+
try {
|
|
1407
|
+
const response = await fetch("/api/workspace/add-ons/upstash/schedule", {
|
|
1408
|
+
method: "POST",
|
|
1409
|
+
headers: { "content-type": "application/json" },
|
|
1410
|
+
body: JSON.stringify({
|
|
1411
|
+
action,
|
|
1412
|
+
productId: "upstash-qstash",
|
|
1413
|
+
objectId,
|
|
1414
|
+
rowId,
|
|
1415
|
+
region: sandboxRow.schedulerRegion || addOnsState.qstashWorkflow?.region || "us-east-1",
|
|
1416
|
+
scheduleId: sandboxRow.scheduleId,
|
|
1417
|
+
}),
|
|
1418
|
+
});
|
|
1419
|
+
const payload = await response.json().catch(() => ({}));
|
|
1420
|
+
if (!response.ok || !payload.ok || !payload.workspaceConfig) {
|
|
1421
|
+
setScheduleError(payload.error || `Could not ${action} the remote schedule.`);
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
setWorkspaceConfig(payload.workspaceConfig);
|
|
1425
|
+
setSaveMessage(action === "pause" ? "Schedule paused." : "Schedule resumed.");
|
|
1426
|
+
return true;
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
setScheduleError(error?.message || `Could not ${action} the remote schedule.`);
|
|
1429
|
+
return false;
|
|
1430
|
+
} finally {
|
|
1431
|
+
setSaving(false);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function openQstashSetup() {
|
|
1436
|
+
router.push("/settings/add-ons");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function openCustomSchedulerSetup() {
|
|
1440
|
+
// Custom schedulers are provider-AGNOSTIC: any governed API Registry row can
|
|
1441
|
+
// be bound via this sandbox row's schedulerRegistryId and is executed by the
|
|
1442
|
+
// generic sandbox-run serverless delegation — no marketplace adapter or
|
|
1443
|
+
// Upstash coupling. Route to the row drawer (where schedulerRegistryId is
|
|
1444
|
+
// editable) when we have a workflow row; otherwise send the operator to
|
|
1445
|
+
// API/Webhooks to register the custom scheduler row first. Never navigate
|
|
1446
|
+
// with empty object/row params.
|
|
1447
|
+
if (objectId && rowId) {
|
|
1448
|
+
router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}&field=${encodeURIComponent("schedulerRegistryId")}`);
|
|
1449
|
+
} else {
|
|
1450
|
+
router.push("/settings/apis-webhooks");
|
|
877
1451
|
}
|
|
878
1452
|
}
|
|
879
1453
|
|
|
@@ -933,18 +1507,6 @@ export default function WorkflowSurface() {
|
|
|
933
1507
|
>
|
|
934
1508
|
<ArrowUp size={13} />
|
|
935
1509
|
</button>
|
|
936
|
-
{sandboxRow && showServerlessUpgrade && (
|
|
937
|
-
<button
|
|
938
|
-
type="button"
|
|
939
|
-
className={"dm-workflow-icon-btn dm-workflow-upgrade-btn" + (isServerlessWorkflow ? " is-serverless" : (upgradeState.showOnboarding ? " is-pulse" : ""))}
|
|
940
|
-
aria-label={isServerlessWorkflow ? "Serverless workflow — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
|
|
941
|
-
data-tooltip={isServerlessWorkflow ? "Serverless — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
|
|
942
|
-
aria-pressed={upgradeOpen}
|
|
943
|
-
onClick={() => setUpgradeOpen((open) => !open)}
|
|
944
|
-
>
|
|
945
|
-
<ArrowUpCircle size={14} />
|
|
946
|
-
</button>
|
|
947
|
-
)}
|
|
948
1510
|
{showDiscardDraft && (
|
|
949
1511
|
<button
|
|
950
1512
|
type="button"
|
|
@@ -1017,42 +1579,23 @@ export default function WorkflowSurface() {
|
|
|
1017
1579
|
</div>
|
|
1018
1580
|
) : null}
|
|
1019
1581
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
{/* Serverless cockpit — same derivation + cockpit interface as the API
|
|
1038
|
-
Registry and sandbox lanes. Toggles patch the sandbox row; deep config
|
|
1039
|
-
(scheduler/adapter) routes to the object's Data Model drawer. */}
|
|
1040
|
-
{sandboxRow && showServerlessUpgrade && upgradeOpen && serverlessState ? (
|
|
1041
|
-
<div className="dm-workflow-upgrade-panel">
|
|
1042
|
-
<div className="dm-workflow-upgrade-panel-head">
|
|
1043
|
-
<span className="dm-api-action-card-eyebrow">Persistence & scheduling</span>
|
|
1044
|
-
<button type="button" className="dm-workflow-icon-btn" aria-label="Close upgrade panel" onClick={() => { setUpgradeOpen(false); dismissUpgradeOnboarding(); }}>
|
|
1045
|
-
<X size={14} />
|
|
1046
|
-
</button>
|
|
1047
|
-
</div>
|
|
1048
|
-
<ApiRegistryCreationCockpit
|
|
1049
|
-
state={serverlessState}
|
|
1050
|
-
onAction={handleUpgradeAction}
|
|
1051
|
-
disabled={saving || publishing || running}
|
|
1052
|
-
eyebrow={isServerlessWorkflow ? "Serverless workflow" : "Upgrade to serverless"}
|
|
1053
|
-
/>
|
|
1054
|
-
</div>
|
|
1055
|
-
) : null}
|
|
1582
|
+
<WorkflowScheduleModal
|
|
1583
|
+
open={scheduleModalOpen}
|
|
1584
|
+
addOn={addOnsState.qstashWorkflow}
|
|
1585
|
+
workflowName={rowId}
|
|
1586
|
+
cadence={scheduleCadence}
|
|
1587
|
+
scheduleTime={scheduleTime}
|
|
1588
|
+
scheduleWeekday={scheduleWeekday}
|
|
1589
|
+
scheduleMonthDay={scheduleMonthDay}
|
|
1590
|
+
errorMessage={scheduleError}
|
|
1591
|
+
disabled={saving || publishing || running}
|
|
1592
|
+
onCadenceChange={updateScheduleCadence}
|
|
1593
|
+
onScheduleTimeChange={setScheduleTime}
|
|
1594
|
+
onScheduleWeekdayChange={setScheduleWeekday}
|
|
1595
|
+
onScheduleMonthDayChange={setScheduleMonthDay}
|
|
1596
|
+
onSubmit={submitQstashSchedule}
|
|
1597
|
+
onClose={() => setScheduleModalOpen(false)}
|
|
1598
|
+
/>
|
|
1056
1599
|
|
|
1057
1600
|
{loading ? (
|
|
1058
1601
|
<p className="dm-workflow-empty">Loading workflow…</p>
|
|
@@ -1107,6 +1650,7 @@ export default function WorkflowSurface() {
|
|
|
1107
1650
|
nodeStatuses={runNodeStatuses}
|
|
1108
1651
|
onNodeStatusClick={(node) => { setSelectedNodeId(String(node?.id || "")); openTraceMode(); }}
|
|
1109
1652
|
statusLabel={isDraftMode ? "Draft" : "Live"}
|
|
1653
|
+
readinessFlags={readinessFlags}
|
|
1110
1654
|
/>
|
|
1111
1655
|
{nextNodeId && (
|
|
1112
1656
|
<button type="button" className="dm-btn-outline dm-orchestration-canvas__add-node" onClick={addNextNode}>
|
|
@@ -1194,6 +1738,135 @@ export default function WorkflowSurface() {
|
|
|
1194
1738
|
activeTab={configTab}
|
|
1195
1739
|
onTabChange={setConfigTab}
|
|
1196
1740
|
onConfigChange={handleNodeConfigChange}
|
|
1741
|
+
readinessFlag={selectedNodeId ? readinessFlags[selectedNodeId] : null}
|
|
1742
|
+
serverlessScheduleOptionAvailable={Boolean(addOnsState.qstashWorkflow || selectedSchedulerRegistryId || schedulerRegistryRows.length)}
|
|
1743
|
+
serverlessScheduleAvailable={remoteScheduleVerified}
|
|
1744
|
+
inputScheduleControls={selectedNode?.type === "input" && selectedNode?.config?.inputMode === "serverless-schedule" ? (
|
|
1745
|
+
<div className="dm-trigger-schedule-config">
|
|
1746
|
+
<span className="dm-field-label">Serverless schedule</span>
|
|
1747
|
+
<dl className="dm-workflow-schedule-state">
|
|
1748
|
+
<div>
|
|
1749
|
+
<dt>Scheduler</dt>
|
|
1750
|
+
<dd>{selectedSchedulerRegistryId || "Install scheduler first"}</dd>
|
|
1751
|
+
</div>
|
|
1752
|
+
<div>
|
|
1753
|
+
<dt>Status</dt>
|
|
1754
|
+
<dd>{remoteScheduleVerified ? (sandboxRow?.schedulerPaused ? "paused" : "bound") : "not bound"}</dd>
|
|
1755
|
+
</div>
|
|
1756
|
+
<div>
|
|
1757
|
+
<dt>Region</dt>
|
|
1758
|
+
<dd>{sandboxRow?.schedulerRegion || selectedSchedulerRow?.region || "pending"}</dd>
|
|
1759
|
+
</div>
|
|
1760
|
+
{remoteScheduleVerified && sandboxRow?.scheduleId ? (
|
|
1761
|
+
<div>
|
|
1762
|
+
<dt>Schedule</dt>
|
|
1763
|
+
<dd>{sandboxRow.scheduleId}</dd>
|
|
1764
|
+
</div>
|
|
1765
|
+
) : null}
|
|
1766
|
+
{remoteScheduleVerified && sandboxRow?.schedulerCron ? (
|
|
1767
|
+
<div>
|
|
1768
|
+
<dt>Cron</dt>
|
|
1769
|
+
<dd>{sandboxRow.schedulerCron}</dd>
|
|
1770
|
+
</div>
|
|
1771
|
+
) : null}
|
|
1772
|
+
{remoteScheduleVerified && sandboxRow?.schedulerInstalledAt ? (
|
|
1773
|
+
<div>
|
|
1774
|
+
<dt>Last sync</dt>
|
|
1775
|
+
<dd>{sandboxRow.schedulerInstalledAt}</dd>
|
|
1776
|
+
</div>
|
|
1777
|
+
) : null}
|
|
1778
|
+
{sandboxRow?.schedulerPausedAt ? (
|
|
1779
|
+
<div>
|
|
1780
|
+
<dt>Paused</dt>
|
|
1781
|
+
<dd>{sandboxRow.schedulerPausedAt}</dd>
|
|
1782
|
+
</div>
|
|
1783
|
+
) : null}
|
|
1784
|
+
</dl>
|
|
1785
|
+
{schedulerRegistryRows.length > 1 ? (
|
|
1786
|
+
<label className="dm-orchestration-config__field">
|
|
1787
|
+
<span>Scheduler registry</span>
|
|
1788
|
+
<select
|
|
1789
|
+
value={selectedSchedulerRegistryId}
|
|
1790
|
+
disabled={Boolean(sandboxRow?.scheduleId) || saving}
|
|
1791
|
+
onChange={(e) => patchSandboxRuntimeFields({ schedulerRegistryId: e.target.value })}
|
|
1792
|
+
>
|
|
1793
|
+
{schedulerRegistryRows.map((entry) => (
|
|
1794
|
+
<option key={entry.integrationId} value={entry.integrationId}>
|
|
1795
|
+
{entry.integrationId}
|
|
1796
|
+
</option>
|
|
1797
|
+
))}
|
|
1798
|
+
</select>
|
|
1799
|
+
</label>
|
|
1800
|
+
) : null}
|
|
1801
|
+
<label className="dm-orchestration-config__field">
|
|
1802
|
+
<span>Cadence</span>
|
|
1803
|
+
<select value={scheduleCadence} onChange={(e) => updateScheduleCadence(e.target.value)}>
|
|
1804
|
+
{SCHEDULE_CADENCE_OPTIONS.map((option) => (
|
|
1805
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
1806
|
+
))}
|
|
1807
|
+
</select>
|
|
1808
|
+
</label>
|
|
1809
|
+
<ScheduleTimeControls value={scheduleTime} onChange={setScheduleTime} />
|
|
1810
|
+
{scheduleCadence === "weekly" ? (
|
|
1811
|
+
<label className="dm-orchestration-config__field">
|
|
1812
|
+
<span>Run day</span>
|
|
1813
|
+
<select value={scheduleWeekday} onChange={(e) => setScheduleWeekday(e.target.value)}>
|
|
1814
|
+
{SCHEDULE_WEEKDAY_OPTIONS.map((option) => (
|
|
1815
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
1816
|
+
))}
|
|
1817
|
+
</select>
|
|
1818
|
+
</label>
|
|
1819
|
+
) : null}
|
|
1820
|
+
{scheduleCadence === "monthly" ? (
|
|
1821
|
+
<label className="dm-orchestration-config__field">
|
|
1822
|
+
<span>Day of month</span>
|
|
1823
|
+
<select value={scheduleMonthDay} onChange={(e) => setScheduleMonthDay(e.target.value)}>
|
|
1824
|
+
{SCHEDULE_MONTH_DAY_OPTIONS.map((option) => (
|
|
1825
|
+
<option key={option.id} value={option.id}>{option.label}</option>
|
|
1826
|
+
))}
|
|
1827
|
+
</select>
|
|
1828
|
+
</label>
|
|
1829
|
+
) : null}
|
|
1830
|
+
{sandboxRow?.lastScheduledRunResponse || sandboxRow?.lastScheduledRunBodyPreview || sandboxRow?.lastScheduledRunStatus ? (
|
|
1831
|
+
<div className="dm-workflow-schedule-last-run">
|
|
1832
|
+
<span>Last run</span>
|
|
1833
|
+
<strong>{sandboxRow?.lastScheduledRunStatus || "pending"}</strong>
|
|
1834
|
+
</div>
|
|
1835
|
+
) : null}
|
|
1836
|
+
{scheduleError ? <p className="dm-workflow-schedule-error" role="alert">{scheduleError}</p> : null}
|
|
1837
|
+
<button
|
|
1838
|
+
type="button"
|
|
1839
|
+
className="dm-btn-outline dm-workflow-schedule-submit"
|
|
1840
|
+
disabled={saving || !addOnsState.qstashWorkflow || !String(scheduleTime || "").trim()}
|
|
1841
|
+
onClick={submitQstashSchedule}
|
|
1842
|
+
>
|
|
1843
|
+
{saving ? "Saving schedule..." : sandboxRow?.scheduleId ? "Update schedule" : "Save schedule"}
|
|
1844
|
+
</button>
|
|
1845
|
+
{!addOnsState.qstashWorkflow ? (
|
|
1846
|
+
<p className="dm-cockpit-step-hint">Install + sync QStash in Workspace Add-ons first, then save the schedule here.</p>
|
|
1847
|
+
) : null}
|
|
1848
|
+
{sandboxRow?.scheduleId ? (
|
|
1849
|
+
<div className="dm-workflow-schedule-actions">
|
|
1850
|
+
<button
|
|
1851
|
+
type="button"
|
|
1852
|
+
className="dm-btn-outline"
|
|
1853
|
+
disabled={saving}
|
|
1854
|
+
onClick={() => controlInstalledScheduler(sandboxRow?.schedulerPaused ? "resume" : "pause")}
|
|
1855
|
+
>
|
|
1856
|
+
{sandboxRow?.schedulerPaused ? "Resume" : "Pause"}
|
|
1857
|
+
</button>
|
|
1858
|
+
<button
|
|
1859
|
+
type="button"
|
|
1860
|
+
className="dm-btn-outline is-danger"
|
|
1861
|
+
disabled={saving}
|
|
1862
|
+
onClick={revertServerlessScheduleToLocal}
|
|
1863
|
+
>
|
|
1864
|
+
Revert to local
|
|
1865
|
+
</button>
|
|
1866
|
+
</div>
|
|
1867
|
+
) : null}
|
|
1868
|
+
</div>
|
|
1869
|
+
) : null}
|
|
1197
1870
|
/>
|
|
1198
1871
|
{graphError && <p className="dm-orchestration-config__error">{graphError}</p>}
|
|
1199
1872
|
</div>
|
|
@@ -1202,7 +1875,7 @@ export default function WorkflowSurface() {
|
|
|
1202
1875
|
</div>
|
|
1203
1876
|
)}
|
|
1204
1877
|
|
|
1205
|
-
{
|
|
1878
|
+
{runMessage && (
|
|
1206
1879
|
<p className="dm-workflow-status-msg">{saveMessage || runMessage}</p>
|
|
1207
1880
|
)}
|
|
1208
1881
|
</section>
|