@burdenoff/fluidgrids-fe-sdk 2026.1115.4 → 2026.1115.5
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/dist/{AiStudioPage-CsS637Bn.js → AiStudioPage-WXzAFKjc.js} +1 -1
- package/dist/{TemplatePreview-VNS0Y4Bb.js → TemplatePreview-wDWBw1Bi.js} +1 -1
- package/dist/{WorkflowBuilder-CISnd6ug.js → WorkflowBuilder-4EidvAbF.js} +1 -1
- package/dist/{WorkflowBuilder-aBnrbP9M.js → WorkflowBuilder-BKSDwVdq.js} +1 -1
- package/dist/{flow-builder-B2vxHaAG.js → flow-builder-BYQbt7mf.js} +3 -3
- package/dist/index.js +6 -6
- package/dist/{node-detail-view-C0cuqQfd.js → node-detail-view-BsTHnPdm.js} +1 -1
- package/dist/{node-editor-tab-body-CsMOa4-Z.js → node-editor-tab-body-D3YGuQD0.js} +1 -1
- package/dist/{quick-config-panel-CIVUr12G.js → quick-config-panel-CtMEb5RX.js} +2 -2
- package/dist/{schedule-panel-CdptgUXs.js → schedule-panel-D_r1fikL.js} +1319 -325
- package/dist/{sidebar-node-editor-BfY2Pjdt.js → sidebar-node-editor-D0QnJDKz.js} +2 -2
- package/dist/templates.js +1 -1
- package/dist/workflow-builder.js +2 -2
- package/dist/workflow-pages.js +3 -3
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@ import { A as useFlowStore } from "./node-operation-selection-DCJ_DDHr.js";
|
|
|
6
6
|
import { $ as resolveLoadOptionsSpec, B as coerceNumber, Dt as useSaveStatusStore, G as paramsFromOperation, L as useRunGuard, Q as optionListValues, U as nodeTitle, W as optionText, X as isStringOptionList, Z as optionListText, cn as structuredParameterType, dn as isParamVisible, et as ConditionTreeBuilder, fn as useFluidGridsAnalytics, it as parseMappingValue, nt as casesFromRules, on as formatParameterValue, ot as boundFieldsOf, pn as openRunQuotaUpgradePrompt, sn as parseParameterValue, st as missingRequiredParameters, t as FluidGridsContext, tt as SwitchRulesEditor, z as asRecord } from "./FluidGridsProvider-CaOI_H63.js";
|
|
7
7
|
import { n as useBuilderReadOnly } from "./read-only-context-Dt9N9SIG.js";
|
|
8
8
|
import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
|
|
9
|
-
import {
|
|
9
|
+
import { m as usePhaseCFlags } from "./feature-flags-B_b8PJ6Z.js";
|
|
10
|
+
import { L as triggerSchedule, T as pauseSchedule, t as backfillSchedule, u as describeSchedule, x as fetchSearchAttributes, z as unpauseSchedule } from "./api-o5UUDbr8.js";
|
|
10
11
|
import { on as PreviewScheduleDocument, t as useAgentDefinitionList, vt as useRunFromNodeEligibilityQuery } from "./useAgentDefinitions-tsGlUO4v.js";
|
|
11
12
|
import { r as describeRule, s as isAgentNodeType } from "./agent-form-D0uLvzx4.js";
|
|
12
13
|
import { C as RegenerateEventRegistrationTokenDocument, S as StopWebhookTestListenerDocument, T as ListenForFormTestSubmissionDocument, _ as webhookDeliveriesPath, b as WorkflowCallInputsDocument, n as PopoverContent$1, r as PopoverTrigger$1, t as Popover$1, v as RunOnSection, w as ListenForWebhookTestEventDocument, x as WebhookTestListenerDocument, y as ConnectionSelector } from "./popover-BtbVqTzP.js";
|
|
@@ -14,7 +15,7 @@ import { a as collectExtensions } from "./quota-Cdt8yz7N.js";
|
|
|
14
15
|
import { BO as ScheduleType, QI as WorkflowScheduleOverlapPolicy } from "./graphql-types-BjCOnRUQ.js";
|
|
15
16
|
import { t as useGlobalStoreClient } from "./useGlobalStoreClient-ZLYG_Xls.js";
|
|
16
17
|
import { a as operatorSpec, c as ruleFromRow, i as normaliseAssertRules, n as describeAssertRuleProblem, o as readAssertRulesSetting, r as emptyAssertRuleRow, s as rowsFromRules, t as ASSERT_OPERATORS } from "./assert-rules-BA2caUAZ.js";
|
|
17
|
-
import { i as NativeSelect, o as StatusPill, t as FieldError, u as useResource } from "./shared-mbyvDPde.js";
|
|
18
|
+
import { i as NativeSelect, l as parseJsonObjectInput, o as StatusPill, t as FieldError, u as useResource } from "./shared-mbyvDPde.js";
|
|
18
19
|
import { r as normalizeCodeLanguage, t as CodeEditorField } from "./code-editor-field-Cv907s3W.js";
|
|
19
20
|
import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
20
21
|
import { Link } from "react-router";
|
|
@@ -2486,7 +2487,7 @@ function defaultScheduleRule() {
|
|
|
2486
2487
|
function isScheduleRuleField(value) {
|
|
2487
2488
|
return typeof value === "string" && SCHEDULE_RULE_FIELDS.some((field) => field === value);
|
|
2488
2489
|
}
|
|
2489
|
-
function isRecord$
|
|
2490
|
+
function isRecord$5(value) {
|
|
2490
2491
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2491
2492
|
}
|
|
2492
2493
|
function toNumber(value) {
|
|
@@ -2585,10 +2586,10 @@ function rulesFromLegacyState(state) {
|
|
|
2585
2586
|
* array and the legacy fields, and always yields at least one rule.
|
|
2586
2587
|
*/
|
|
2587
2588
|
function normalizeScheduleState(state) {
|
|
2588
|
-
const source = isRecord$
|
|
2589
|
+
const source = isRecord$5(state) ? state : {};
|
|
2589
2590
|
let rules;
|
|
2590
2591
|
if (Array.isArray(source.rules)) {
|
|
2591
|
-
rules = source.rules.filter(isRecord$
|
|
2592
|
+
rules = source.rules.filter(isRecord$5).map(normalizeScheduleRule);
|
|
2592
2593
|
if (rules.length === 0) rules = [defaultScheduleRule()];
|
|
2593
2594
|
} else rules = rulesFromLegacyState(source);
|
|
2594
2595
|
const timezone = toTrimmedString(source.timezone) || "UTC";
|
|
@@ -2603,10 +2604,10 @@ function normalizeScheduleState(state) {
|
|
|
2603
2604
|
}
|
|
2604
2605
|
/** Whether the saved state predates `rules[]` (so the editor is showing a conversion). */
|
|
2605
2606
|
function isLegacyScheduleState(state) {
|
|
2606
|
-
if (!isRecord$
|
|
2607
|
+
if (!isRecord$5(state) || Array.isArray(state.rules)) return false;
|
|
2607
2608
|
return state.mode !== void 0 || toTrimmedString(state.cronExpression) !== "" || toNumber(state.interval) !== void 0;
|
|
2608
2609
|
}
|
|
2609
|
-
function inRange(value, min, max) {
|
|
2610
|
+
function inRange$1(value, min, max) {
|
|
2610
2611
|
return value !== void 0 && Number.isInteger(value) && value >= min && value <= max;
|
|
2611
2612
|
}
|
|
2612
2613
|
/** Humanised text for a cron expression, or null when cronstrue rejects it. */
|
|
@@ -2629,30 +2630,30 @@ function validateRule(rule) {
|
|
|
2629
2630
|
const issues = [];
|
|
2630
2631
|
switch (rule.field) {
|
|
2631
2632
|
case "seconds":
|
|
2632
|
-
if (!inRange(rule.secondsInterval, 1, 59)) issues.push("secondsRange");
|
|
2633
|
+
if (!inRange$1(rule.secondsInterval, 1, 59)) issues.push("secondsRange");
|
|
2633
2634
|
break;
|
|
2634
2635
|
case "minutes":
|
|
2635
|
-
if (!inRange(rule.minutesInterval, 1, 59)) issues.push("minutesRange");
|
|
2636
|
+
if (!inRange$1(rule.minutesInterval, 1, 59)) issues.push("minutesRange");
|
|
2636
2637
|
break;
|
|
2637
2638
|
case "hours":
|
|
2638
|
-
if (!inRange(rule.hoursInterval, 1, 23)) issues.push("hoursRange");
|
|
2639
|
+
if (!inRange$1(rule.hoursInterval, 1, 23)) issues.push("hoursRange");
|
|
2639
2640
|
break;
|
|
2640
2641
|
case "days":
|
|
2641
|
-
if (!inRange(rule.daysInterval, 1, 31)) issues.push("daysRange");
|
|
2642
|
+
if (!inRange$1(rule.daysInterval, 1, 31)) issues.push("daysRange");
|
|
2642
2643
|
break;
|
|
2643
2644
|
case "weeks": {
|
|
2644
2645
|
if (!isPositiveInteger(rule.weeksInterval)) issues.push("weeksMin");
|
|
2645
2646
|
const days = rule.triggerAtDay;
|
|
2646
2647
|
if (days === void 0 || days.length === 0) issues.push("weekdaysRequired");
|
|
2647
2648
|
else {
|
|
2648
|
-
if (!days.every((day) => inRange(day, 0, 6))) issues.push("weekdaysRange");
|
|
2649
|
+
if (!days.every((day) => inRange$1(day, 0, 6))) issues.push("weekdaysRange");
|
|
2649
2650
|
if (new Set(days).size !== days.length) issues.push("weekdaysRepeat");
|
|
2650
2651
|
}
|
|
2651
2652
|
break;
|
|
2652
2653
|
}
|
|
2653
2654
|
case "months":
|
|
2654
2655
|
if (!isPositiveInteger(rule.monthsInterval)) issues.push("monthsMin");
|
|
2655
|
-
if (rule.triggerAtDayOfMonth !== void 0 && !inRange(rule.triggerAtDayOfMonth, 1, 31)) issues.push("dayOfMonthRange");
|
|
2656
|
+
if (rule.triggerAtDayOfMonth !== void 0 && !inRange$1(rule.triggerAtDayOfMonth, 1, 31)) issues.push("dayOfMonthRange");
|
|
2656
2657
|
break;
|
|
2657
2658
|
case "cronExpression": {
|
|
2658
2659
|
const text = (rule.expression ?? "").trim();
|
|
@@ -2662,8 +2663,8 @@ function validateRule(rule) {
|
|
|
2662
2663
|
return describeCron(text) === null ? ["cronInvalid"] : [];
|
|
2663
2664
|
}
|
|
2664
2665
|
}
|
|
2665
|
-
if (rule.triggerAtHour !== void 0 && !inRange(rule.triggerAtHour, 0, 23)) issues.push("hourRange");
|
|
2666
|
-
if (rule.triggerAtMinute !== void 0 && !inRange(rule.triggerAtMinute, 0, 59)) issues.push("minuteRange");
|
|
2666
|
+
if (rule.triggerAtHour !== void 0 && !inRange$1(rule.triggerAtHour, 0, 23)) issues.push("hourRange");
|
|
2667
|
+
if (rule.triggerAtMinute !== void 0 && !inRange$1(rule.triggerAtMinute, 0, 59)) issues.push("minuteRange");
|
|
2667
2668
|
return issues;
|
|
2668
2669
|
}
|
|
2669
2670
|
/**
|
|
@@ -3296,7 +3297,7 @@ const CHOICE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3296
3297
|
const MULTI_CHOICE_TYPES = /* @__PURE__ */ new Set(["multiselect", "checkboxes"]);
|
|
3297
3298
|
/** Types the service renders without a placeholder. */
|
|
3298
3299
|
const NO_PLACEHOLDER_TYPES = /* @__PURE__ */ new Set(["checkbox", "hidden"]);
|
|
3299
|
-
function isRecord$
|
|
3300
|
+
function isRecord$4(value) {
|
|
3300
3301
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3301
3302
|
}
|
|
3302
3303
|
/**
|
|
@@ -3337,7 +3338,7 @@ function readFormFieldsSetting(value) {
|
|
|
3337
3338
|
return null;
|
|
3338
3339
|
}
|
|
3339
3340
|
}
|
|
3340
|
-
if (!Array.isArray(parsed) || !parsed.every(isRecord$
|
|
3341
|
+
if (!Array.isArray(parsed) || !parsed.every(isRecord$4)) return null;
|
|
3341
3342
|
return parsed.map((field) => ({ ...field }));
|
|
3342
3343
|
}
|
|
3343
3344
|
/** The text the JSON view shows for a setting. */
|
|
@@ -3418,7 +3419,7 @@ function formFieldOptions(field) {
|
|
|
3418
3419
|
if (!Array.isArray(field.options)) return [];
|
|
3419
3420
|
return field.options.map((option) => {
|
|
3420
3421
|
if (typeof option === "string") return option;
|
|
3421
|
-
if (isRecord$
|
|
3422
|
+
if (isRecord$4(option)) return String(option.value ?? option.label ?? "");
|
|
3422
3423
|
return option === void 0 || option === null ? "" : String(option);
|
|
3423
3424
|
});
|
|
3424
3425
|
}
|
|
@@ -3872,7 +3873,7 @@ const TOOL_APPROVAL_RULES = [
|
|
|
3872
3873
|
function isRule(value) {
|
|
3873
3874
|
return typeof value === "string" && TOOL_APPROVAL_RULES.includes(value);
|
|
3874
3875
|
}
|
|
3875
|
-
function isRecord$
|
|
3876
|
+
function isRecord$3(value) {
|
|
3876
3877
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3877
3878
|
}
|
|
3878
3879
|
function parsed(value) {
|
|
@@ -3886,11 +3887,11 @@ function parsed(value) {
|
|
|
3886
3887
|
/** The rows of a stored setting; rows with an unknown rule read as `ask`. */
|
|
3887
3888
|
function readToolApprovalRows(value) {
|
|
3888
3889
|
const raw = parsed(value);
|
|
3889
|
-
if (Array.isArray(raw)) return raw.filter(isRecord$
|
|
3890
|
+
if (Array.isArray(raw)) return raw.filter(isRecord$3).map((row) => ({
|
|
3890
3891
|
toolName: typeof row.toolName === "string" ? row.toolName : "",
|
|
3891
3892
|
rule: isRule(row.rule) ? row.rule : "ask"
|
|
3892
3893
|
}));
|
|
3893
|
-
if (isRecord$
|
|
3894
|
+
if (isRecord$3(raw)) return Object.entries(raw).map(([toolName, rule]) => ({
|
|
3894
3895
|
toolName,
|
|
3895
3896
|
rule: isRule(rule) ? rule : "ask"
|
|
3896
3897
|
}));
|
|
@@ -3907,9 +3908,9 @@ function connectedToolNames(agentId, nodes, edges) {
|
|
|
3907
3908
|
for (const edge of edges) {
|
|
3908
3909
|
if (edge.source !== agentId || edge.sourceHandle !== "Tool") continue;
|
|
3909
3910
|
const node = edge.target ? byId.get(edge.target) : void 0;
|
|
3910
|
-
const args = isRecord$
|
|
3911
|
-
const state = isRecord$
|
|
3912
|
-
const name = isRecord$
|
|
3911
|
+
const args = isRecord$3(node?.data) ? node.data.args : void 0;
|
|
3912
|
+
const state = isRecord$3(args) ? args.state : void 0;
|
|
3913
|
+
const name = isRecord$3(state) ? state.toolName : void 0;
|
|
3913
3914
|
if (typeof name === "string" && name.trim()) names.add(name.trim());
|
|
3914
3915
|
}
|
|
3915
3916
|
return [...names];
|
|
@@ -4472,17 +4473,17 @@ const PLATFORM_EVENT_SOURCES_QUERY = gql`
|
|
|
4472
4473
|
/** The list hardly changes; one read per few minutes is plenty. */
|
|
4473
4474
|
const CACHE_TTL_MS$1 = 18e4;
|
|
4474
4475
|
const caches$1 = /* @__PURE__ */ new WeakMap();
|
|
4475
|
-
function isRecord$
|
|
4476
|
+
function isRecord$2(value) {
|
|
4476
4477
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4477
4478
|
}
|
|
4478
4479
|
/** The well-formed sources of a `platformEventSources` response; anything else is dropped. */
|
|
4479
4480
|
function readPlatformEventSources(data) {
|
|
4480
|
-
const list = isRecord$
|
|
4481
|
+
const list = isRecord$2(data) ? data.platformEventSources : void 0;
|
|
4481
4482
|
if (!Array.isArray(list)) return [];
|
|
4482
4483
|
const sources = [];
|
|
4483
4484
|
for (const item of list) {
|
|
4484
|
-
if (!isRecord$
|
|
4485
|
-
const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$
|
|
4485
|
+
if (!isRecord$2(item) || typeof item.module !== "string") continue;
|
|
4486
|
+
const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$2(type) && typeof type.name === "string" ? [{
|
|
4486
4487
|
name: type.name,
|
|
4487
4488
|
actions: Array.isArray(type.actions) ? type.actions.filter((action) => typeof action === "string") : []
|
|
4488
4489
|
}] : []) : [];
|
|
@@ -4749,16 +4750,16 @@ const MY_TIME_TRIGGERS_QUERY = gql`
|
|
|
4749
4750
|
*/
|
|
4750
4751
|
const CACHE_TTL_MS = 3e4;
|
|
4751
4752
|
const caches = /* @__PURE__ */ new WeakMap();
|
|
4752
|
-
function isRecord(value) {
|
|
4753
|
+
function isRecord$1(value) {
|
|
4753
4754
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4754
4755
|
}
|
|
4755
4756
|
/** The well-formed triggers of a `timeCampusMyTimeTriggers` response, sorted by name. */
|
|
4756
4757
|
function readTimeTriggers(data) {
|
|
4757
|
-
const list = isRecord(data) ? data.timeCampusMyTimeTriggers : void 0;
|
|
4758
|
+
const list = isRecord$1(data) ? data.timeCampusMyTimeTriggers : void 0;
|
|
4758
4759
|
if (!Array.isArray(list)) return [];
|
|
4759
4760
|
const triggers = [];
|
|
4760
4761
|
for (const item of list) {
|
|
4761
|
-
if (!isRecord(item) || typeof item.id !== "string" || !item.id) continue;
|
|
4762
|
+
if (!isRecord$1(item) || typeof item.id !== "string" || !item.id) continue;
|
|
4762
4763
|
triggers.push({
|
|
4763
4764
|
id: item.id,
|
|
4764
4765
|
name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : item.id,
|
|
@@ -8333,15 +8334,998 @@ function NodeExecutionActions({ targetNodeId, nodes, edges, logs, workflowKey, w
|
|
|
8333
8334
|
});
|
|
8334
8335
|
}
|
|
8335
8336
|
|
|
8337
|
+
// src/builder/components/run-control/schedule-spec.ts
|
|
8338
|
+
const SCHEDULE_SPEC_KINDS = [
|
|
8339
|
+
"cron",
|
|
8340
|
+
"calendar",
|
|
8341
|
+
"interval"
|
|
8342
|
+
];
|
|
8343
|
+
/** Fields a spec may carry beside its one kind (SDK SCHEDULE_SPEC_FIELDS). */
|
|
8344
|
+
const SCHEDULE_SPEC_FIELDS = [
|
|
8345
|
+
"excludeCalendars",
|
|
8346
|
+
"startAt",
|
|
8347
|
+
"endAt",
|
|
8348
|
+
"timezone",
|
|
8349
|
+
"jitter"
|
|
8350
|
+
];
|
|
8351
|
+
const CALENDAR_FIELDS = [
|
|
8352
|
+
{
|
|
8353
|
+
name: "second",
|
|
8354
|
+
min: 0,
|
|
8355
|
+
max: 59,
|
|
8356
|
+
defaultsToZero: true
|
|
8357
|
+
},
|
|
8358
|
+
{
|
|
8359
|
+
name: "minute",
|
|
8360
|
+
min: 0,
|
|
8361
|
+
max: 59,
|
|
8362
|
+
defaultsToZero: true
|
|
8363
|
+
},
|
|
8364
|
+
{
|
|
8365
|
+
name: "hour",
|
|
8366
|
+
min: 0,
|
|
8367
|
+
max: 23,
|
|
8368
|
+
defaultsToZero: true
|
|
8369
|
+
},
|
|
8370
|
+
{
|
|
8371
|
+
name: "dayOfMonth",
|
|
8372
|
+
min: 1,
|
|
8373
|
+
max: 31,
|
|
8374
|
+
defaultsToZero: false
|
|
8375
|
+
},
|
|
8376
|
+
{
|
|
8377
|
+
name: "month",
|
|
8378
|
+
min: 1,
|
|
8379
|
+
max: 12,
|
|
8380
|
+
defaultsToZero: false
|
|
8381
|
+
},
|
|
8382
|
+
{
|
|
8383
|
+
name: "dayOfWeek",
|
|
8384
|
+
min: 0,
|
|
8385
|
+
max: 6,
|
|
8386
|
+
defaultsToZero: false
|
|
8387
|
+
},
|
|
8388
|
+
{
|
|
8389
|
+
name: "year",
|
|
8390
|
+
min: 1970,
|
|
8391
|
+
max: 9999,
|
|
8392
|
+
defaultsToZero: false
|
|
8393
|
+
}
|
|
8394
|
+
];
|
|
8395
|
+
/** The longest jitter the service accepts (one week, like the catch-up window). */
|
|
8396
|
+
const MAX_JITTER_SECONDS = 604800;
|
|
8397
|
+
/** The cron a new cron spec starts with (every hour, on the hour). */
|
|
8398
|
+
const DEFAULT_SPEC_CRON = "0 * * * *";
|
|
8399
|
+
function isRecord(value) {
|
|
8400
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8401
|
+
}
|
|
8402
|
+
/** The spec's kind, or null when the trigger has no usable spec. */
|
|
8403
|
+
function specKindOf(spec) {
|
|
8404
|
+
if (!isRecord(spec)) return null;
|
|
8405
|
+
const kinds = SCHEDULE_SPEC_KINDS.filter((kind) => spec[kind] !== void 0);
|
|
8406
|
+
return kinds.length === 1 ? kinds[0] ?? null : null;
|
|
8407
|
+
}
|
|
8408
|
+
/** The spec of a node state, as a record (an empty record when absent or not an object). */
|
|
8409
|
+
function specOf(state) {
|
|
8410
|
+
return isRecord(state.spec) ? state.spec : {};
|
|
8411
|
+
}
|
|
8412
|
+
/** A first entry for each kind, so a spec switched to it is valid at once. */
|
|
8413
|
+
function seedFor(kind) {
|
|
8414
|
+
if (kind === "cron") return DEFAULT_SPEC_CRON;
|
|
8415
|
+
if (kind === "calendar") return [{
|
|
8416
|
+
hour: [9],
|
|
8417
|
+
minute: [0]
|
|
8418
|
+
}];
|
|
8419
|
+
return [{ every: "1h" }];
|
|
8420
|
+
}
|
|
8421
|
+
/**
|
|
8422
|
+
* The spec switched to `kind`, keeping its window, timezone, jitter and
|
|
8423
|
+
* exclusions; `null` drops the spec (the trigger's rules apply again, and
|
|
8424
|
+
* the spec-only fields go with it, because they cannot exist without one).
|
|
8425
|
+
*/
|
|
8426
|
+
function withSpecKind(spec, kind) {
|
|
8427
|
+
if (kind === null) return void 0;
|
|
8428
|
+
const current = isRecord(spec) ? spec : {};
|
|
8429
|
+
if (specKindOf(current) === kind) return current;
|
|
8430
|
+
const next = {};
|
|
8431
|
+
for (const field of SCHEDULE_SPEC_FIELDS) if (current[field] !== void 0) next[field] = current[field];
|
|
8432
|
+
next[kind] = seedFor(kind);
|
|
8433
|
+
return next;
|
|
8434
|
+
}
|
|
8435
|
+
/** The spec with one field set (undefined removes it). */
|
|
8436
|
+
function withSpecField(spec, field, value) {
|
|
8437
|
+
const next = { ...isRecord(spec) ? spec : {} };
|
|
8438
|
+
if (value === void 0) delete next[field];
|
|
8439
|
+
else next[field] = value;
|
|
8440
|
+
return next;
|
|
8441
|
+
}
|
|
8442
|
+
const SHORT_DURATION = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/;
|
|
8443
|
+
const ISO_DURATION = /^P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
8444
|
+
const UNIT_MS = {
|
|
8445
|
+
ms: 1,
|
|
8446
|
+
s: 1e3,
|
|
8447
|
+
m: 6e4,
|
|
8448
|
+
h: 36e5,
|
|
8449
|
+
d: 864e5,
|
|
8450
|
+
w: 6048e5
|
|
8451
|
+
};
|
|
8452
|
+
/**
|
|
8453
|
+
* Milliseconds of a duration as the IR accepts it (the SDK's `durationMs`
|
|
8454
|
+
* with bare numbers in seconds): `90`, `'30s'`, `'15m'`, `'2h'`, `'1d'`,
|
|
8455
|
+
* `'1w'`, `'PT1H30M'`. Undefined when the value is not a duration.
|
|
8456
|
+
*/
|
|
8457
|
+
function durationMsOf(value) {
|
|
8458
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value * 1e3 : void 0;
|
|
8459
|
+
if (typeof value !== "string") return void 0;
|
|
8460
|
+
const text = value.trim();
|
|
8461
|
+
const short = SHORT_DURATION.exec(text);
|
|
8462
|
+
if (short?.[1] !== void 0 && short[2] !== void 0) {
|
|
8463
|
+
const unit = UNIT_MS[short[2]];
|
|
8464
|
+
return unit === void 0 ? void 0 : Number(short[1]) * unit;
|
|
8465
|
+
}
|
|
8466
|
+
const iso = ISO_DURATION.exec(text);
|
|
8467
|
+
if (!iso || text === "P" || text === "PT") return void 0;
|
|
8468
|
+
const [, weeks, days, hours, minutes, seconds] = iso;
|
|
8469
|
+
if ([
|
|
8470
|
+
weeks,
|
|
8471
|
+
days,
|
|
8472
|
+
hours,
|
|
8473
|
+
minutes,
|
|
8474
|
+
seconds
|
|
8475
|
+
].every((p) => p === void 0)) return void 0;
|
|
8476
|
+
return Number(weeks ?? 0) * 6048e5 + Number(days ?? 0) * 864e5 + Number(hours ?? 0) * 36e5 + Number(minutes ?? 0) * 6e4 + Number(seconds ?? 0) * 1e3;
|
|
8477
|
+
}
|
|
8478
|
+
/** The text a stored duration shows as (a number of seconds shows as `<n>s`). */
|
|
8479
|
+
function formatDuration(value) {
|
|
8480
|
+
if (typeof value === "number") return `${value}s`;
|
|
8481
|
+
return typeof value === "string" ? value : "";
|
|
8482
|
+
}
|
|
8483
|
+
/**
|
|
8484
|
+
* A duration typed in the panel. Empty is "no value"; digits alone are
|
|
8485
|
+
* seconds and stored as a number; anything else is kept as typed when the
|
|
8486
|
+
* IR reads it as a duration. `wholeSeconds` refuses fractions of a second
|
|
8487
|
+
* (interval entries run on the scheduler's one-second grid).
|
|
8488
|
+
*/
|
|
8489
|
+
function parseDurationInput(text, options = {}) {
|
|
8490
|
+
const trimmed = text.trim();
|
|
8491
|
+
if (trimmed === "") return {
|
|
8492
|
+
ok: true,
|
|
8493
|
+
value: void 0
|
|
8494
|
+
};
|
|
8495
|
+
const value = /^\d+$/.test(trimmed) ? Number(trimmed) : trimmed;
|
|
8496
|
+
const ms = durationMsOf(value);
|
|
8497
|
+
if (ms === void 0) return {
|
|
8498
|
+
ok: false,
|
|
8499
|
+
error: "Enter a duration such as 30s, 15m, 2h, 1d or PT1H30M"
|
|
8500
|
+
};
|
|
8501
|
+
if (options.positive && ms <= 0) return {
|
|
8502
|
+
ok: false,
|
|
8503
|
+
error: "The duration must be longer than zero"
|
|
8504
|
+
};
|
|
8505
|
+
if (options.wholeSeconds && ms % 1e3 !== 0) return {
|
|
8506
|
+
ok: false,
|
|
8507
|
+
error: "Use whole seconds"
|
|
8508
|
+
};
|
|
8509
|
+
if (options.maxSeconds !== void 0 && ms > options.maxSeconds * 1e3) return {
|
|
8510
|
+
ok: false,
|
|
8511
|
+
error: `At most ${options.maxSeconds / 86400} days`
|
|
8512
|
+
};
|
|
8513
|
+
return {
|
|
8514
|
+
ok: true,
|
|
8515
|
+
value
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
/** An interval entry's own problem (the service's `intervalEntryOf`), or null. */
|
|
8519
|
+
function intervalEntryProblem(entry) {
|
|
8520
|
+
if (!isRecord(entry)) return "Each interval entry must be an object";
|
|
8521
|
+
const every = durationMsOf(entry.every);
|
|
8522
|
+
if (every === void 0 || every < 1e3 || every % 1e3 !== 0) return "Every must be a whole number of seconds, at least 1s";
|
|
8523
|
+
const offset = entry.offset === void 0 ? 0 : durationMsOf(entry.offset);
|
|
8524
|
+
if (offset === void 0 || offset % 1e3 !== 0 || offset >= every) return "The offset must be a whole number of seconds below every";
|
|
8525
|
+
return null;
|
|
8526
|
+
}
|
|
8527
|
+
/** The text a stored calendar field shows as: `1,15`, `1-5`, `0-59/15`, `*`. */
|
|
8528
|
+
function formatCalendarField(value, field) {
|
|
8529
|
+
if (value === void 0) return "";
|
|
8530
|
+
return (Array.isArray(value) ? value : [value]).map((entry) => {
|
|
8531
|
+
if (typeof entry === "number") return String(entry);
|
|
8532
|
+
if (!isRecord(entry) || typeof entry.start !== "number") return "?";
|
|
8533
|
+
const start = entry.start;
|
|
8534
|
+
const end = typeof entry.end === "number" ? entry.end : start;
|
|
8535
|
+
const step = typeof entry.step === "number" ? entry.step : 1;
|
|
8536
|
+
if (start === field.min && end === field.max) return step === 1 ? "*" : `*/${step}`;
|
|
8537
|
+
const range = end === start ? String(start) : `${start}-${end}`;
|
|
8538
|
+
return step === 1 ? range : `${range}/${step}`;
|
|
8539
|
+
}).join(",");
|
|
8540
|
+
}
|
|
8541
|
+
function inRange(n, field) {
|
|
8542
|
+
return Number.isInteger(n) && n >= field.min && n <= field.max;
|
|
8543
|
+
}
|
|
8544
|
+
/**
|
|
8545
|
+
* A calendar field typed in the panel: comma-separated values `n`, ranges
|
|
8546
|
+
* `a-b`, steps `a-b/s`, `a/s` (to the field's end), `*` (every value) and
|
|
8547
|
+
* `*\/s`. Empty is "no value" (0 for second, minute and hour; any for the
|
|
8548
|
+
* others). `*` on a date field is the same as empty, so it is stored as
|
|
8549
|
+
* absent.
|
|
8550
|
+
*/
|
|
8551
|
+
function parseCalendarField(text, field) {
|
|
8552
|
+
const trimmed = text.trim();
|
|
8553
|
+
if (trimmed === "") return {
|
|
8554
|
+
ok: true,
|
|
8555
|
+
value: void 0
|
|
8556
|
+
};
|
|
8557
|
+
const values = [];
|
|
8558
|
+
for (const raw of trimmed.split(",")) {
|
|
8559
|
+
const token = raw.trim();
|
|
8560
|
+
const match = /^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/.exec(token);
|
|
8561
|
+
if (!match) return {
|
|
8562
|
+
ok: false,
|
|
8563
|
+
error: `"${token}" is not a value, a range (a-b) or a step (a-b/s)`
|
|
8564
|
+
};
|
|
8565
|
+
const [, first, last, stepText] = match;
|
|
8566
|
+
const step = stepText === void 0 ? 1 : Number(stepText);
|
|
8567
|
+
if (step < 1) return {
|
|
8568
|
+
ok: false,
|
|
8569
|
+
error: "A step must be at least 1"
|
|
8570
|
+
};
|
|
8571
|
+
if (first === "*") {
|
|
8572
|
+
if (last !== void 0) return {
|
|
8573
|
+
ok: false,
|
|
8574
|
+
error: `"${token}": * cannot start a range`
|
|
8575
|
+
};
|
|
8576
|
+
if (step === 1 && !field.defaultsToZero && trimmed === "*") return {
|
|
8577
|
+
ok: true,
|
|
8578
|
+
value: void 0
|
|
8579
|
+
};
|
|
8580
|
+
values.push(step === 1 ? {
|
|
8581
|
+
start: field.min,
|
|
8582
|
+
end: field.max
|
|
8583
|
+
} : {
|
|
8584
|
+
start: field.min,
|
|
8585
|
+
end: field.max,
|
|
8586
|
+
step
|
|
8587
|
+
});
|
|
8588
|
+
continue;
|
|
8589
|
+
}
|
|
8590
|
+
const start = Number(first);
|
|
8591
|
+
const end = last !== void 0 ? Number(last) : stepText !== void 0 ? field.max : start;
|
|
8592
|
+
if (!inRange(start, field) || !inRange(end, field)) return {
|
|
8593
|
+
ok: false,
|
|
8594
|
+
error: `Values must be between ${field.min} and ${field.max}`
|
|
8595
|
+
};
|
|
8596
|
+
if (end < start) return {
|
|
8597
|
+
ok: false,
|
|
8598
|
+
error: `"${token}": the range ends before it starts`
|
|
8599
|
+
};
|
|
8600
|
+
if (end === start && step === 1) values.push(start);
|
|
8601
|
+
else values.push(step === 1 ? {
|
|
8602
|
+
start,
|
|
8603
|
+
end
|
|
8604
|
+
} : {
|
|
8605
|
+
start,
|
|
8606
|
+
end,
|
|
8607
|
+
step
|
|
8608
|
+
});
|
|
8609
|
+
}
|
|
8610
|
+
return {
|
|
8611
|
+
ok: true,
|
|
8612
|
+
value: values
|
|
8613
|
+
};
|
|
8614
|
+
}
|
|
8615
|
+
/** A calendar entry with one field set (undefined removes it). */
|
|
8616
|
+
function withCalendarField(entry, field, value) {
|
|
8617
|
+
const next = { ...isRecord(entry) ? entry : {} };
|
|
8618
|
+
if (value === void 0 || value === "") delete next[field];
|
|
8619
|
+
else next[field] = value;
|
|
8620
|
+
return next;
|
|
8621
|
+
}
|
|
8622
|
+
/** A calendar entry's own problem (field values out of range), or null. */
|
|
8623
|
+
function calendarEntryProblem(entry) {
|
|
8624
|
+
if (!isRecord(entry)) return "Each calendar entry must be an object";
|
|
8625
|
+
for (const field of CALENDAR_FIELDS) {
|
|
8626
|
+
const value = entry[field.name];
|
|
8627
|
+
if (value === void 0) continue;
|
|
8628
|
+
const parsed = parseCalendarField(formatCalendarField(value, field), field);
|
|
8629
|
+
if (!parsed.ok) return `${field.name}: ${parsed.error}`;
|
|
8630
|
+
}
|
|
8631
|
+
return null;
|
|
8632
|
+
}
|
|
8633
|
+
/** Whether `zone` is an IANA time zone this browser knows. */
|
|
8634
|
+
function isTimeZone(zone) {
|
|
8635
|
+
if (zone.trim() === "") return false;
|
|
8636
|
+
try {
|
|
8637
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
8638
|
+
return true;
|
|
8639
|
+
} catch {
|
|
8640
|
+
return false;
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
/** The `datetime-local` text of a stored ISO date-time (in the browser's zone). */
|
|
8644
|
+
function localInputOf(value) {
|
|
8645
|
+
if (typeof value !== "string") return "";
|
|
8646
|
+
const ms = Date.parse(value);
|
|
8647
|
+
if (Number.isNaN(ms)) return "";
|
|
8648
|
+
const date = new Date(ms);
|
|
8649
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8650
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
8651
|
+
}
|
|
8652
|
+
/** The ISO date-time of a `datetime-local` text; undefined when empty or invalid. */
|
|
8653
|
+
function isoOfLocalInput(text) {
|
|
8654
|
+
if (text.trim() === "") return void 0;
|
|
8655
|
+
const ms = new Date(text).getTime();
|
|
8656
|
+
return Number.isNaN(ms) ? void 0 : new Date(ms).toISOString();
|
|
8657
|
+
}
|
|
8658
|
+
/** The spec's own problem with its window (`endAt` after `startAt`), or null. */
|
|
8659
|
+
function windowProblem(spec) {
|
|
8660
|
+
const start = typeof spec.startAt === "string" ? Date.parse(spec.startAt) : NaN;
|
|
8661
|
+
const end = typeof spec.endAt === "string" ? Date.parse(spec.endAt) : NaN;
|
|
8662
|
+
if (!Number.isNaN(start) && !Number.isNaN(end) && end <= start) return "The end must be after the start";
|
|
8663
|
+
return null;
|
|
8664
|
+
}
|
|
8665
|
+
/** The JSON text of a stored object field (`scheduleContext`, `scheduleSearchAttributes`). */
|
|
8666
|
+
function jsonTextOf(value) {
|
|
8667
|
+
return isRecord(value) ? JSON.stringify(value, null, 2) : "";
|
|
8668
|
+
}
|
|
8669
|
+
/** The names of a search attribute object that no definition declares. */
|
|
8670
|
+
function undefinedAttributeNames(attributes, definedNames) {
|
|
8671
|
+
if (!isRecord(attributes)) return [];
|
|
8672
|
+
const defined = new Set(definedNames);
|
|
8673
|
+
return Object.keys(attributes).filter((name) => !defined.has(name));
|
|
8674
|
+
}
|
|
8675
|
+
|
|
8676
|
+
// src/builder/components/run-control/schedule-spec-editor.tsx
|
|
8677
|
+
/**
|
|
8678
|
+
* The spec half of the Schedule Trigger's schedule panel (BOFF-13524, spec
|
|
8679
|
+
* code-first 11 §2.33 and §2.16): which fire times the schedule uses (the
|
|
8680
|
+
* trigger's own rules, or a cron, calendar or interval spec), the spec's
|
|
8681
|
+
* window, timezone, jitter and excluded calendars, and the schedule context
|
|
8682
|
+
* and search attributes every run it starts carries.
|
|
8683
|
+
*
|
|
8684
|
+
* Everything is written to the node's `args.state` (`spec`,
|
|
8685
|
+
* `scheduleContext`, `scheduleSearchAttributes`), which is what the service
|
|
8686
|
+
* reads when the workflow is saved. A field that is being typed and does not
|
|
8687
|
+
* parse yet is kept in the field and not written, with the reason under it,
|
|
8688
|
+
* so the node never holds a value the save would refuse for its shape.
|
|
8689
|
+
*/
|
|
8690
|
+
/**
|
|
8691
|
+
* A text field whose typed text is kept while it does not parse. The parsed
|
|
8692
|
+
* value is written through `onValue` only when it parses; when the stored
|
|
8693
|
+
* value changes from elsewhere (undo, the canvas JSON), the text follows it.
|
|
8694
|
+
*/
|
|
8695
|
+
function useDraft(formatted, parse, onValue) {
|
|
8696
|
+
const [text, setText] = useState(formatted);
|
|
8697
|
+
const [error, setError] = useState(null);
|
|
8698
|
+
const [followed, setFollowed] = useState(formatted);
|
|
8699
|
+
if (formatted !== followed) {
|
|
8700
|
+
setFollowed(formatted);
|
|
8701
|
+
setText(formatted);
|
|
8702
|
+
setError(null);
|
|
8703
|
+
}
|
|
8704
|
+
return {
|
|
8705
|
+
text,
|
|
8706
|
+
error,
|
|
8707
|
+
onChange: (next) => {
|
|
8708
|
+
setText(next);
|
|
8709
|
+
const parsed = parse(next);
|
|
8710
|
+
if (!parsed.ok) {
|
|
8711
|
+
setError(parsed.error);
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
setError(null);
|
|
8715
|
+
onValue(parsed.value);
|
|
8716
|
+
}
|
|
8717
|
+
};
|
|
8718
|
+
}
|
|
8719
|
+
function DraftInput({ id, label, formatted, parse, onValue, disabled, placeholder, testId, hint }) {
|
|
8720
|
+
const draft = useDraft(formatted, parse, onValue);
|
|
8721
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8722
|
+
className: "space-y-1",
|
|
8723
|
+
children: [
|
|
8724
|
+
/* @__PURE__ */ jsx(Label, {
|
|
8725
|
+
htmlFor: id,
|
|
8726
|
+
className: "text-xs",
|
|
8727
|
+
children: label
|
|
8728
|
+
}),
|
|
8729
|
+
/* @__PURE__ */ jsx(Input, {
|
|
8730
|
+
id,
|
|
8731
|
+
value: draft.text,
|
|
8732
|
+
disabled,
|
|
8733
|
+
placeholder,
|
|
8734
|
+
onChange: (event) => draft.onChange(event.target.value),
|
|
8735
|
+
"aria-invalid": draft.error ? true : void 0,
|
|
8736
|
+
"data-testid": testId
|
|
8737
|
+
}),
|
|
8738
|
+
hint && !draft.error && /* @__PURE__ */ jsx("p", {
|
|
8739
|
+
className: "text-xs text-muted-foreground",
|
|
8740
|
+
children: hint
|
|
8741
|
+
}),
|
|
8742
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
8743
|
+
message: draft.error,
|
|
8744
|
+
testId: `${testId}-error`
|
|
8745
|
+
})
|
|
8746
|
+
]
|
|
8747
|
+
});
|
|
8748
|
+
}
|
|
8749
|
+
function DraftJson({ id, label, value, onValue, disabled, placeholder, testId }) {
|
|
8750
|
+
const draft = useDraft(jsonTextOf(value), (text) => {
|
|
8751
|
+
if (text.trim() === "") return {
|
|
8752
|
+
ok: true,
|
|
8753
|
+
value: void 0
|
|
8754
|
+
};
|
|
8755
|
+
const parsed = parseJsonObjectInput(text);
|
|
8756
|
+
return parsed.ok ? {
|
|
8757
|
+
ok: true,
|
|
8758
|
+
value: parsed.value
|
|
8759
|
+
} : parsed;
|
|
8760
|
+
}, onValue);
|
|
8761
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8762
|
+
className: "space-y-1.5",
|
|
8763
|
+
children: [
|
|
8764
|
+
/* @__PURE__ */ jsx(Label, {
|
|
8765
|
+
htmlFor: id,
|
|
8766
|
+
children: label
|
|
8767
|
+
}),
|
|
8768
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
8769
|
+
id,
|
|
8770
|
+
value: draft.text,
|
|
8771
|
+
disabled,
|
|
8772
|
+
rows: 4,
|
|
8773
|
+
placeholder,
|
|
8774
|
+
className: "font-mono text-xs",
|
|
8775
|
+
onChange: (event) => draft.onChange(event.target.value),
|
|
8776
|
+
"aria-invalid": draft.error ? true : void 0,
|
|
8777
|
+
"data-testid": testId
|
|
8778
|
+
}),
|
|
8779
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
8780
|
+
message: draft.error,
|
|
8781
|
+
testId: `${testId}-error`
|
|
8782
|
+
})
|
|
8783
|
+
]
|
|
8784
|
+
});
|
|
8785
|
+
}
|
|
8786
|
+
const FIELD_LABELS = {
|
|
8787
|
+
second: ["fluidgrids.builder.schedule.spec.second", "Second"],
|
|
8788
|
+
minute: ["fluidgrids.builder.schedule.spec.minute", "Minute"],
|
|
8789
|
+
hour: ["fluidgrids.builder.schedule.spec.hour", "Hour"],
|
|
8790
|
+
dayOfMonth: ["fluidgrids.builder.schedule.spec.dayOfMonth", "Day of month"],
|
|
8791
|
+
month: ["fluidgrids.builder.schedule.spec.month", "Month"],
|
|
8792
|
+
dayOfWeek: ["fluidgrids.builder.schedule.spec.dayOfWeek", "Day of week"],
|
|
8793
|
+
year: ["fluidgrids.builder.schedule.spec.year", "Year"]
|
|
8794
|
+
};
|
|
8795
|
+
/** One calendar entry (a spec entry or an excluded calendar). */
|
|
8796
|
+
function CalendarEntryEditor({ entry, index, testId, onChange, onRemove, disabled }) {
|
|
8797
|
+
const tr = useTr();
|
|
8798
|
+
const problem = calendarEntryProblem(entry);
|
|
8799
|
+
const comment = isRecord(entry) && typeof entry.comment === "string" ? entry.comment : "";
|
|
8800
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8801
|
+
className: "space-y-2 rounded-md border border-border p-3",
|
|
8802
|
+
"data-testid": `${testId}-${index}`,
|
|
8803
|
+
children: [
|
|
8804
|
+
/* @__PURE__ */ jsx("div", {
|
|
8805
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
|
|
8806
|
+
children: CALENDAR_FIELDS.map((field) => {
|
|
8807
|
+
const [key, fallback] = FIELD_LABELS[field.name];
|
|
8808
|
+
return /* @__PURE__ */ jsx(DraftInput, {
|
|
8809
|
+
id: `${testId}-${index}-${field.name}`,
|
|
8810
|
+
label: tr(key, fallback),
|
|
8811
|
+
formatted: formatCalendarField(isRecord(entry) ? entry[field.name] : void 0, field),
|
|
8812
|
+
parse: (text) => parseCalendarField(text, field),
|
|
8813
|
+
onValue: (value) => onChange(withCalendarField(entry, field.name, value)),
|
|
8814
|
+
disabled,
|
|
8815
|
+
placeholder: field.defaultsToZero ? "0" : "*",
|
|
8816
|
+
testId: `${testId}-${index}-${field.name}`
|
|
8817
|
+
}, field.name);
|
|
8818
|
+
})
|
|
8819
|
+
}),
|
|
8820
|
+
/* @__PURE__ */ jsx(Input, {
|
|
8821
|
+
value: comment,
|
|
8822
|
+
disabled,
|
|
8823
|
+
maxLength: 200,
|
|
8824
|
+
placeholder: tr("fluidgrids.builder.schedule.spec.comment", "Comment (optional)"),
|
|
8825
|
+
onChange: (event) => onChange(withCalendarField(entry, "comment", event.target.value)),
|
|
8826
|
+
"data-testid": `${testId}-${index}-comment`
|
|
8827
|
+
}),
|
|
8828
|
+
/* @__PURE__ */ jsx(FieldError, { message: problem }),
|
|
8829
|
+
onRemove && !disabled && /* @__PURE__ */ jsxs(Button$1, {
|
|
8830
|
+
size: "sm",
|
|
8831
|
+
variant: "ghost",
|
|
8832
|
+
onClick: onRemove,
|
|
8833
|
+
"data-testid": `${testId}-${index}-remove`,
|
|
8834
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
8835
|
+
icon: "mdi:delete-outline",
|
|
8836
|
+
className: "mr-1",
|
|
8837
|
+
width: 14
|
|
8838
|
+
}), tr("fluidgrids.builder.schedule.spec.remove", "Remove")]
|
|
8839
|
+
})
|
|
8840
|
+
]
|
|
8841
|
+
});
|
|
8842
|
+
}
|
|
8843
|
+
function listOf(value) {
|
|
8844
|
+
return Array.isArray(value) ? value : [];
|
|
8845
|
+
}
|
|
8846
|
+
function replaced(list, index, entry) {
|
|
8847
|
+
return list.map((current, at) => at === index ? entry : current);
|
|
8848
|
+
}
|
|
8849
|
+
function removed(list, index) {
|
|
8850
|
+
return list.filter((_, at) => at !== index);
|
|
8851
|
+
}
|
|
8852
|
+
let lastEntryKey = 0;
|
|
8853
|
+
function freshEntryKeys(count) {
|
|
8854
|
+
return Array.from({ length: count }, () => {
|
|
8855
|
+
lastEntryKey += 1;
|
|
8856
|
+
return `entry-${lastEntryKey}`;
|
|
8857
|
+
});
|
|
8858
|
+
}
|
|
8859
|
+
/**
|
|
8860
|
+
* Stable React keys for a list of plain entries, which carry no ids of
|
|
8861
|
+
* their own. Adding and removing through the returned helpers keeps every
|
|
8862
|
+
* other row's key, so a row's half-typed text stays with that row. A change
|
|
8863
|
+
* of length from elsewhere (undo, the canvas JSON) starts a fresh set of
|
|
8864
|
+
* keys, which remounts the rows with the stored values.
|
|
8865
|
+
*/
|
|
8866
|
+
function useEntryKeys(length) {
|
|
8867
|
+
const [keys, setKeys] = useState(() => freshEntryKeys(length));
|
|
8868
|
+
let current = keys;
|
|
8869
|
+
if (keys.length !== length) {
|
|
8870
|
+
current = freshEntryKeys(length);
|
|
8871
|
+
setKeys(current);
|
|
8872
|
+
}
|
|
8873
|
+
return {
|
|
8874
|
+
keys: current,
|
|
8875
|
+
add: () => setKeys([...current, ...freshEntryKeys(1)]),
|
|
8876
|
+
remove: (index) => setKeys(current.filter((_, at) => at !== index))
|
|
8877
|
+
};
|
|
8878
|
+
}
|
|
8879
|
+
/** A list of calendar entries: the spec's calendar or its exclusions. */
|
|
8880
|
+
function CalendarEntryList({ entries, minimum, newEntry, onEntries, disabled, entryTestId, addTestId, addLabel }) {
|
|
8881
|
+
const rows = useEntryKeys(entries.length);
|
|
8882
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [entries.map((entry, index) => /* @__PURE__ */ jsx(CalendarEntryEditor, {
|
|
8883
|
+
entry,
|
|
8884
|
+
index,
|
|
8885
|
+
testId: entryTestId,
|
|
8886
|
+
disabled,
|
|
8887
|
+
onChange: (next) => onEntries(replaced(entries, index, next)),
|
|
8888
|
+
onRemove: entries.length > minimum ? () => {
|
|
8889
|
+
rows.remove(index);
|
|
8890
|
+
onEntries(removed(entries, index));
|
|
8891
|
+
} : null
|
|
8892
|
+
}, rows.keys[index])), !disabled && /* @__PURE__ */ jsxs(Button$1, {
|
|
8893
|
+
size: "sm",
|
|
8894
|
+
variant: "outline",
|
|
8895
|
+
onClick: () => {
|
|
8896
|
+
rows.add();
|
|
8897
|
+
onEntries([...entries, newEntry]);
|
|
8898
|
+
},
|
|
8899
|
+
"data-testid": addTestId,
|
|
8900
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
8901
|
+
icon: "mdi:plus",
|
|
8902
|
+
className: "mr-1",
|
|
8903
|
+
width: 14
|
|
8904
|
+
}), addLabel]
|
|
8905
|
+
})] });
|
|
8906
|
+
}
|
|
8907
|
+
/** One interval entry: its length and optional offset. */
|
|
8908
|
+
function IntervalEntryEditor({ entry, index, onChange, onRemove, disabled }) {
|
|
8909
|
+
const tr = useTr();
|
|
8910
|
+
const record = isRecord(entry) ? entry : {};
|
|
8911
|
+
const testId = `schedule-spec-interval-${index}`;
|
|
8912
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8913
|
+
className: "space-y-2 rounded-md border border-border p-3",
|
|
8914
|
+
"data-testid": `schedule-spec-interval-entry-${index}`,
|
|
8915
|
+
children: [
|
|
8916
|
+
/* @__PURE__ */ jsxs("div", {
|
|
8917
|
+
className: "grid gap-2 sm:grid-cols-2",
|
|
8918
|
+
children: [/* @__PURE__ */ jsx(DraftInput, {
|
|
8919
|
+
id: `${testId}-every`,
|
|
8920
|
+
label: tr("fluidgrids.builder.schedule.spec.every", "Every"),
|
|
8921
|
+
formatted: formatDuration(record.every),
|
|
8922
|
+
parse: (text) => text.trim() === "" ? {
|
|
8923
|
+
ok: false,
|
|
8924
|
+
error: "Every is required"
|
|
8925
|
+
} : parseDurationInput(text, {
|
|
8926
|
+
wholeSeconds: true,
|
|
8927
|
+
positive: true
|
|
8928
|
+
}),
|
|
8929
|
+
onValue: (value) => onChange({
|
|
8930
|
+
...record,
|
|
8931
|
+
every: value
|
|
8932
|
+
}),
|
|
8933
|
+
disabled,
|
|
8934
|
+
placeholder: "15m",
|
|
8935
|
+
testId: `${testId}-every`
|
|
8936
|
+
}), /* @__PURE__ */ jsx(DraftInput, {
|
|
8937
|
+
id: `${testId}-offset`,
|
|
8938
|
+
label: tr("fluidgrids.builder.schedule.spec.offset", "Offset (optional)"),
|
|
8939
|
+
formatted: formatDuration(record.offset),
|
|
8940
|
+
parse: (text) => parseDurationInput(text, { wholeSeconds: true }),
|
|
8941
|
+
onValue: (value) => {
|
|
8942
|
+
const next = { ...record };
|
|
8943
|
+
if (value === void 0) delete next.offset;
|
|
8944
|
+
else next.offset = value;
|
|
8945
|
+
onChange(next);
|
|
8946
|
+
},
|
|
8947
|
+
disabled,
|
|
8948
|
+
placeholder: "0s",
|
|
8949
|
+
testId: `${testId}-offset`
|
|
8950
|
+
})]
|
|
8951
|
+
}),
|
|
8952
|
+
/* @__PURE__ */ jsx(FieldError, { message: intervalEntryProblem(entry) }),
|
|
8953
|
+
onRemove && !disabled && /* @__PURE__ */ jsxs(Button$1, {
|
|
8954
|
+
size: "sm",
|
|
8955
|
+
variant: "ghost",
|
|
8956
|
+
onClick: onRemove,
|
|
8957
|
+
"data-testid": `${testId}-remove`,
|
|
8958
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
8959
|
+
icon: "mdi:delete-outline",
|
|
8960
|
+
className: "mr-1",
|
|
8961
|
+
width: 14
|
|
8962
|
+
}), tr("fluidgrids.builder.schedule.spec.remove", "Remove")]
|
|
8963
|
+
})
|
|
8964
|
+
]
|
|
8965
|
+
});
|
|
8966
|
+
}
|
|
8967
|
+
function IntervalList({ entries, onEntries, disabled }) {
|
|
8968
|
+
const tr = useTr();
|
|
8969
|
+
const rows = useEntryKeys(entries.length);
|
|
8970
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8971
|
+
className: "space-y-2",
|
|
8972
|
+
"data-testid": "schedule-spec-interval",
|
|
8973
|
+
children: [
|
|
8974
|
+
/* @__PURE__ */ jsx("p", {
|
|
8975
|
+
className: "text-xs text-muted-foreground",
|
|
8976
|
+
children: tr("fluidgrids.builder.schedule.spec.intervalHint", "An interval fires at every multiple of its length since 1970-01-01 UTC, moved by the offset.")
|
|
8977
|
+
}),
|
|
8978
|
+
entries.map((entry, index) => /* @__PURE__ */ jsx(IntervalEntryEditor, {
|
|
8979
|
+
entry,
|
|
8980
|
+
index,
|
|
8981
|
+
disabled,
|
|
8982
|
+
onChange: (next) => onEntries(replaced(entries, index, next)),
|
|
8983
|
+
onRemove: entries.length > 1 ? () => {
|
|
8984
|
+
rows.remove(index);
|
|
8985
|
+
onEntries(removed(entries, index));
|
|
8986
|
+
} : null
|
|
8987
|
+
}, rows.keys[index])),
|
|
8988
|
+
!disabled && /* @__PURE__ */ jsxs(Button$1, {
|
|
8989
|
+
size: "sm",
|
|
8990
|
+
variant: "outline",
|
|
8991
|
+
onClick: () => {
|
|
8992
|
+
rows.add();
|
|
8993
|
+
onEntries([...entries, { every: "1h" }]);
|
|
8994
|
+
},
|
|
8995
|
+
"data-testid": "schedule-spec-interval-add",
|
|
8996
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
8997
|
+
icon: "mdi:plus",
|
|
8998
|
+
className: "mr-1",
|
|
8999
|
+
width: 14
|
|
9000
|
+
}), tr("fluidgrids.builder.schedule.spec.addInterval", "Add an interval")]
|
|
9001
|
+
})
|
|
9002
|
+
]
|
|
9003
|
+
});
|
|
9004
|
+
}
|
|
9005
|
+
function parseCron(text) {
|
|
9006
|
+
const trimmed = text.trim();
|
|
9007
|
+
const fields = trimmed.split(/\s+/);
|
|
9008
|
+
return trimmed !== "" && fields.length >= 5 && fields.length <= 6 ? {
|
|
9009
|
+
ok: true,
|
|
9010
|
+
value: trimmed
|
|
9011
|
+
} : {
|
|
9012
|
+
ok: false,
|
|
9013
|
+
error: "Enter a cron expression with 5 or 6 fields"
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
function parseTimeZone(text) {
|
|
9017
|
+
const zone = text.trim();
|
|
9018
|
+
if (zone === "") return {
|
|
9019
|
+
ok: true,
|
|
9020
|
+
value: void 0
|
|
9021
|
+
};
|
|
9022
|
+
return isTimeZone(zone) ? {
|
|
9023
|
+
ok: true,
|
|
9024
|
+
value: zone
|
|
9025
|
+
} : {
|
|
9026
|
+
ok: false,
|
|
9027
|
+
error: "Enter an IANA time zone, such as Asia/Kolkata"
|
|
9028
|
+
};
|
|
9029
|
+
}
|
|
9030
|
+
/** The choice between the trigger's rules and a cron, calendar or interval. */
|
|
9031
|
+
function SpecKindField({ storedSpec, kind, disabled, onSpec }) {
|
|
9032
|
+
const tr = useTr();
|
|
9033
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9034
|
+
className: "space-y-1.5",
|
|
9035
|
+
children: [
|
|
9036
|
+
/* @__PURE__ */ jsx(Label, {
|
|
9037
|
+
htmlFor: "schedule-spec-kind",
|
|
9038
|
+
children: tr("fluidgrids.builder.schedule.spec.kind", "When the schedule fires")
|
|
9039
|
+
}),
|
|
9040
|
+
/* @__PURE__ */ jsxs(NativeSelect, {
|
|
9041
|
+
id: "schedule-spec-kind",
|
|
9042
|
+
value: kind ?? "",
|
|
9043
|
+
disabled,
|
|
9044
|
+
onChange: (value) => onSpec(withSpecKind(storedSpec, SCHEDULE_SPEC_KINDS.find((candidate) => candidate === value) ?? null)),
|
|
9045
|
+
testId: "schedule-spec-kind",
|
|
9046
|
+
children: [
|
|
9047
|
+
/* @__PURE__ */ jsx("option", {
|
|
9048
|
+
value: "",
|
|
9049
|
+
children: tr("fluidgrids.builder.schedule.spec.rules", "The trigger's rules (Parameters tab)")
|
|
9050
|
+
}),
|
|
9051
|
+
/* @__PURE__ */ jsx("option", {
|
|
9052
|
+
value: "cron",
|
|
9053
|
+
children: tr("fluidgrids.builder.schedule.spec.cron", "Cron expression")
|
|
9054
|
+
}),
|
|
9055
|
+
/* @__PURE__ */ jsx("option", {
|
|
9056
|
+
value: "calendar",
|
|
9057
|
+
children: tr("fluidgrids.builder.schedule.spec.calendar", "Calendar entries")
|
|
9058
|
+
}),
|
|
9059
|
+
/* @__PURE__ */ jsx("option", {
|
|
9060
|
+
value: "interval",
|
|
9061
|
+
children: tr("fluidgrids.builder.schedule.spec.interval", "Intervals")
|
|
9062
|
+
})
|
|
9063
|
+
]
|
|
9064
|
+
}),
|
|
9065
|
+
/* @__PURE__ */ jsx("p", {
|
|
9066
|
+
className: "text-xs text-muted-foreground",
|
|
9067
|
+
children: kind ? tr("fluidgrids.builder.schedule.spec.replacesRules", "These fire times replace the trigger's rules. Choose the trigger's rules again to drop them, with the window, jitter and excluded calendars.") : tr("fluidgrids.builder.schedule.spec.rulesHint", "A start and end, jitter and excluded calendars need fire times defined here.")
|
|
9068
|
+
}),
|
|
9069
|
+
storedSpec !== void 0 && !kind && /* @__PURE__ */ jsx(FieldError, {
|
|
9070
|
+
message: tr("fluidgrids.builder.schedule.spec.invalid", "The stored spec needs exactly one of cron, calendar or interval. Choose one to replace it."),
|
|
9071
|
+
testId: "schedule-spec-invalid"
|
|
9072
|
+
})
|
|
9073
|
+
]
|
|
9074
|
+
});
|
|
9075
|
+
}
|
|
9076
|
+
/** The fire times themselves, for the chosen kind. */
|
|
9077
|
+
function SpecFireTimes({ kind, spec, setField, disabled }) {
|
|
9078
|
+
const tr = useTr();
|
|
9079
|
+
if (kind === "cron") return /* @__PURE__ */ jsx(DraftInput, {
|
|
9080
|
+
id: "schedule-spec-cron",
|
|
9081
|
+
label: tr("fluidgrids.builder.schedule.spec.cronLabel", "Cron (5 or 6 fields)"),
|
|
9082
|
+
formatted: typeof spec.cron === "string" ? spec.cron : "",
|
|
9083
|
+
parse: parseCron,
|
|
9084
|
+
onValue: (value) => setField("cron", value),
|
|
9085
|
+
disabled,
|
|
9086
|
+
placeholder: "0 9 * * 1-5",
|
|
9087
|
+
testId: "schedule-spec-cron"
|
|
9088
|
+
});
|
|
9089
|
+
if (kind === "interval") return /* @__PURE__ */ jsx(IntervalList, {
|
|
9090
|
+
entries: listOf(spec.interval),
|
|
9091
|
+
onEntries: (next) => setField("interval", next),
|
|
9092
|
+
disabled
|
|
9093
|
+
});
|
|
9094
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9095
|
+
className: "space-y-2",
|
|
9096
|
+
"data-testid": "schedule-spec-calendar",
|
|
9097
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
9098
|
+
className: "text-xs text-muted-foreground",
|
|
9099
|
+
children: tr("fluidgrids.builder.schedule.spec.calendarHint", "Each field takes values, ranges and steps (1,15 or 1-5 or 0-59/15). Empty second, minute and hour mean 0; empty date fields mean any. Day of week 0 is Sunday. Every field of an entry must match.")
|
|
9100
|
+
}), /* @__PURE__ */ jsx(CalendarEntryList, {
|
|
9101
|
+
entries: listOf(spec.calendar),
|
|
9102
|
+
minimum: 1,
|
|
9103
|
+
newEntry: {
|
|
9104
|
+
hour: [9],
|
|
9105
|
+
minute: [0]
|
|
9106
|
+
},
|
|
9107
|
+
onEntries: (next) => setField("calendar", next),
|
|
9108
|
+
disabled,
|
|
9109
|
+
entryTestId: "schedule-spec-calendar-entry",
|
|
9110
|
+
addTestId: "schedule-spec-calendar-add",
|
|
9111
|
+
addLabel: tr("fluidgrids.builder.schedule.spec.addCalendar", "Add a calendar entry")
|
|
9112
|
+
})]
|
|
9113
|
+
});
|
|
9114
|
+
}
|
|
9115
|
+
const WHOLE_DAY_EXCLUSION = {
|
|
9116
|
+
second: [{
|
|
9117
|
+
start: 0,
|
|
9118
|
+
end: 59
|
|
9119
|
+
}],
|
|
9120
|
+
minute: [{
|
|
9121
|
+
start: 0,
|
|
9122
|
+
end: 59
|
|
9123
|
+
}],
|
|
9124
|
+
hour: [{
|
|
9125
|
+
start: 0,
|
|
9126
|
+
end: 23
|
|
9127
|
+
}],
|
|
9128
|
+
dayOfWeek: [0, 6]
|
|
9129
|
+
};
|
|
9130
|
+
function DateTimeField({ id, label, value, onValue, disabled }) {
|
|
9131
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9132
|
+
className: "space-y-1",
|
|
9133
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
9134
|
+
htmlFor: id,
|
|
9135
|
+
className: "text-xs",
|
|
9136
|
+
children: label
|
|
9137
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
9138
|
+
id,
|
|
9139
|
+
type: "datetime-local",
|
|
9140
|
+
disabled,
|
|
9141
|
+
value: localInputOf(value),
|
|
9142
|
+
onChange: (event) => onValue(isoOfLocalInput(event.target.value)),
|
|
9143
|
+
"data-testid": id
|
|
9144
|
+
})]
|
|
9145
|
+
});
|
|
9146
|
+
}
|
|
9147
|
+
/** The window, time zone, jitter and excluded calendars of a spec. */
|
|
9148
|
+
function SpecOptions({ kind, spec, setField, disabled }) {
|
|
9149
|
+
const tr = useTr();
|
|
9150
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9151
|
+
className: "space-y-3",
|
|
9152
|
+
"data-testid": "schedule-spec-options",
|
|
9153
|
+
children: [
|
|
9154
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9155
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
9156
|
+
children: [/* @__PURE__ */ jsx(DateTimeField, {
|
|
9157
|
+
id: "schedule-spec-start",
|
|
9158
|
+
label: tr("fluidgrids.builder.schedule.spec.startAt", "Start (optional)"),
|
|
9159
|
+
value: spec.startAt,
|
|
9160
|
+
onValue: (value) => setField("startAt", value),
|
|
9161
|
+
disabled
|
|
9162
|
+
}), /* @__PURE__ */ jsx(DateTimeField, {
|
|
9163
|
+
id: "schedule-spec-end",
|
|
9164
|
+
label: tr("fluidgrids.builder.schedule.spec.endAt", "End (optional)"),
|
|
9165
|
+
value: spec.endAt,
|
|
9166
|
+
onValue: (value) => setField("endAt", value),
|
|
9167
|
+
disabled
|
|
9168
|
+
})]
|
|
9169
|
+
}),
|
|
9170
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
9171
|
+
message: windowProblem(spec),
|
|
9172
|
+
testId: "schedule-spec-window-error"
|
|
9173
|
+
}),
|
|
9174
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9175
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
9176
|
+
children: [/* @__PURE__ */ jsx(DraftInput, {
|
|
9177
|
+
id: "schedule-spec-timezone",
|
|
9178
|
+
label: tr("fluidgrids.builder.schedule.spec.timezone", "Time zone (optional)"),
|
|
9179
|
+
formatted: typeof spec.timezone === "string" ? spec.timezone : "",
|
|
9180
|
+
parse: parseTimeZone,
|
|
9181
|
+
onValue: (value) => setField("timezone", value),
|
|
9182
|
+
disabled,
|
|
9183
|
+
placeholder: "Asia/Kolkata",
|
|
9184
|
+
hint: kind === "interval" ? tr("fluidgrids.builder.schedule.spec.timezoneInterval", "Intervals always run on UTC; the time zone applies to excluded calendars.") : tr("fluidgrids.builder.schedule.spec.timezoneHint", "Empty uses the trigger's time zone."),
|
|
9185
|
+
testId: "schedule-spec-timezone"
|
|
9186
|
+
}), /* @__PURE__ */ jsx(DraftInput, {
|
|
9187
|
+
id: "schedule-spec-jitter",
|
|
9188
|
+
label: tr("fluidgrids.builder.schedule.spec.jitter", "Jitter (optional)"),
|
|
9189
|
+
formatted: formatDuration(spec.jitter),
|
|
9190
|
+
parse: (text) => parseDurationInput(text, { maxSeconds: MAX_JITTER_SECONDS }),
|
|
9191
|
+
onValue: (value) => setField("jitter", value),
|
|
9192
|
+
disabled,
|
|
9193
|
+
placeholder: "30s",
|
|
9194
|
+
hint: tr("fluidgrids.builder.schedule.spec.jitterHint", "Each fire waits a random time up to this long."),
|
|
9195
|
+
testId: "schedule-spec-jitter"
|
|
9196
|
+
})]
|
|
9197
|
+
}),
|
|
9198
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9199
|
+
className: "space-y-2",
|
|
9200
|
+
"data-testid": "schedule-spec-excludes",
|
|
9201
|
+
children: [
|
|
9202
|
+
/* @__PURE__ */ jsx("p", {
|
|
9203
|
+
className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
9204
|
+
children: tr("fluidgrids.builder.schedule.spec.excludes", "Excluded calendars")
|
|
9205
|
+
}),
|
|
9206
|
+
/* @__PURE__ */ jsx("p", {
|
|
9207
|
+
className: "text-xs text-muted-foreground",
|
|
9208
|
+
children: tr("fluidgrids.builder.schedule.spec.excludesHint", "A fire time that matches any of these is skipped (recorded as excluded). Empty second, minute and hour mean 0, so leave them out only for a whole-day exclusion with 0-59 and 0-23.")
|
|
9209
|
+
}),
|
|
9210
|
+
/* @__PURE__ */ jsx(CalendarEntryList, {
|
|
9211
|
+
entries: listOf(spec.excludeCalendars),
|
|
9212
|
+
minimum: 0,
|
|
9213
|
+
newEntry: WHOLE_DAY_EXCLUSION,
|
|
9214
|
+
onEntries: (next) => setField("excludeCalendars", next.length > 0 ? next : void 0),
|
|
9215
|
+
disabled,
|
|
9216
|
+
entryTestId: "schedule-spec-exclude",
|
|
9217
|
+
addTestId: "schedule-spec-exclude-add",
|
|
9218
|
+
addLabel: tr("fluidgrids.builder.schedule.spec.addExclude", "Add an excluded calendar")
|
|
9219
|
+
})
|
|
9220
|
+
]
|
|
9221
|
+
})
|
|
9222
|
+
]
|
|
9223
|
+
});
|
|
9224
|
+
}
|
|
9225
|
+
/** What the search attribute definitions say about the typed names. */
|
|
9226
|
+
function SearchAttributeNotes({ value, searchAttributesOn }) {
|
|
9227
|
+
const tr = useTr();
|
|
9228
|
+
const definitions = useResource(searchAttributesOn ? "schedule:search-attribute-definitions" : null, searchAttributesOn, fetchSearchAttributes);
|
|
9229
|
+
if (!searchAttributesOn) return /* @__PURE__ */ jsx("p", {
|
|
9230
|
+
className: "text-xs text-muted-foreground",
|
|
9231
|
+
"data-testid": "schedule-search-attributes-off",
|
|
9232
|
+
children: tr("fluidgrids.builder.schedule.spec.searchAttributesOff", "Search attributes are applied to runs only while search attributes are enabled for this workspace.")
|
|
9233
|
+
});
|
|
9234
|
+
if (!definitions.data) return null;
|
|
9235
|
+
const names = definitions.data.map((definition) => definition.name);
|
|
9236
|
+
const unknownNames = undefinedAttributeNames(value, names);
|
|
9237
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
|
|
9238
|
+
className: "text-xs text-muted-foreground",
|
|
9239
|
+
"data-testid": "schedule-search-attributes-defined",
|
|
9240
|
+
children: names.length > 0 ? tr("fluidgrids.builder.schedule.spec.definedAttributes", "Defined: {{names}}", { names: names.join(", ") }) : tr("fluidgrids.builder.schedule.spec.noDefinedAttributes", "No search attributes are defined yet (Settings, Run control).")
|
|
9241
|
+
}), unknownNames.length > 0 && /* @__PURE__ */ jsx(FieldError, {
|
|
9242
|
+
message: tr("fluidgrids.builder.schedule.spec.undefinedAttributes", "Not defined, so left out when a run starts: {{names}}", { names: unknownNames.join(", ") }),
|
|
9243
|
+
testId: "schedule-search-attributes-undefined"
|
|
9244
|
+
})] });
|
|
9245
|
+
}
|
|
9246
|
+
function ScheduleSpecEditor({ state, writeField, canUpdate, searchAttributesOn }) {
|
|
9247
|
+
const tr = useTr();
|
|
9248
|
+
const spec = specOf(state);
|
|
9249
|
+
const kind = specKindOf(state.spec);
|
|
9250
|
+
const disabled = !canUpdate;
|
|
9251
|
+
const writeSpec = (next) => writeField("spec", next);
|
|
9252
|
+
const setField = (field, value) => writeSpec(withSpecField(spec, field, value));
|
|
9253
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9254
|
+
className: "space-y-5",
|
|
9255
|
+
"data-testid": "schedule-spec",
|
|
9256
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
9257
|
+
className: "space-y-3",
|
|
9258
|
+
children: [
|
|
9259
|
+
/* @__PURE__ */ jsx("h4", {
|
|
9260
|
+
className: "text-sm font-semibold text-foreground",
|
|
9261
|
+
children: tr("fluidgrids.builder.schedule.spec.title", "Fire times")
|
|
9262
|
+
}),
|
|
9263
|
+
/* @__PURE__ */ jsx(SpecKindField, {
|
|
9264
|
+
storedSpec: state.spec,
|
|
9265
|
+
kind,
|
|
9266
|
+
disabled,
|
|
9267
|
+
onSpec: writeSpec
|
|
9268
|
+
}),
|
|
9269
|
+
kind && /* @__PURE__ */ jsx(SpecFireTimes, {
|
|
9270
|
+
kind,
|
|
9271
|
+
spec,
|
|
9272
|
+
setField,
|
|
9273
|
+
disabled
|
|
9274
|
+
}),
|
|
9275
|
+
kind && /* @__PURE__ */ jsx(SpecOptions, {
|
|
9276
|
+
kind,
|
|
9277
|
+
spec,
|
|
9278
|
+
setField,
|
|
9279
|
+
disabled
|
|
9280
|
+
})
|
|
9281
|
+
]
|
|
9282
|
+
}), /* @__PURE__ */ jsxs("section", {
|
|
9283
|
+
className: "space-y-3",
|
|
9284
|
+
"data-testid": "schedule-run-fields",
|
|
9285
|
+
children: [
|
|
9286
|
+
/* @__PURE__ */ jsx("h4", {
|
|
9287
|
+
className: "text-sm font-semibold text-foreground",
|
|
9288
|
+
children: tr("fluidgrids.builder.schedule.spec.runFields", "Every run the schedule starts")
|
|
9289
|
+
}),
|
|
9290
|
+
/* @__PURE__ */ jsx(DraftJson, {
|
|
9291
|
+
id: "schedule-context",
|
|
9292
|
+
label: tr("fluidgrids.builder.schedule.spec.context", "Schedule context (JSON object, the run reads it as context.scheduleContext)"),
|
|
9293
|
+
value: state.scheduleContext,
|
|
9294
|
+
onValue: (value) => writeField("scheduleContext", value),
|
|
9295
|
+
disabled,
|
|
9296
|
+
placeholder: "{\"region\": \"eu\"}",
|
|
9297
|
+
testId: "schedule-context"
|
|
9298
|
+
}),
|
|
9299
|
+
/* @__PURE__ */ jsx(DraftJson, {
|
|
9300
|
+
id: "schedule-search-attributes",
|
|
9301
|
+
label: tr("fluidgrids.builder.schedule.spec.searchAttributes", "Search attributes (JSON object of defined attributes)"),
|
|
9302
|
+
value: state.scheduleSearchAttributes,
|
|
9303
|
+
onValue: (value) => writeField("scheduleSearchAttributes", value),
|
|
9304
|
+
disabled,
|
|
9305
|
+
placeholder: "{\"CustomerTier\": \"gold\"}",
|
|
9306
|
+
testId: "schedule-search-attributes"
|
|
9307
|
+
}),
|
|
9308
|
+
/* @__PURE__ */ jsx(SearchAttributeNotes, {
|
|
9309
|
+
value: state.scheduleSearchAttributes,
|
|
9310
|
+
searchAttributesOn
|
|
9311
|
+
})
|
|
9312
|
+
]
|
|
9313
|
+
})]
|
|
9314
|
+
});
|
|
9315
|
+
}
|
|
9316
|
+
|
|
8336
9317
|
// src/builder/components/run-control/schedule-panel.tsx
|
|
8337
9318
|
/**
|
|
8338
9319
|
* The schedule panel of a Schedule Trigger node (BOFF-9906, spec code-first
|
|
8339
9320
|
* 11 §2.16 and §2.33), shown in the node's detail view while
|
|
8340
|
-
* fluidgrids.schedule-policies is on. It edits
|
|
8341
|
-
* reads from the node's state when the workflow is saved
|
|
8342
|
-
* catch-up window, pause on failure, limited actions)
|
|
8343
|
-
*
|
|
8344
|
-
*
|
|
9321
|
+
* fluidgrids.schedule-policies is on. It edits every schedule field the
|
|
9322
|
+
* service reads from the node's state when the workflow is saved: the policy
|
|
9323
|
+
* (overlap policy, catch-up window, pause on failure, limited actions), the
|
|
9324
|
+
* spec (cron, calendar or interval fire times, start and end, time zone,
|
|
9325
|
+
* jitter, excluded calendars) and the schedule context and search
|
|
9326
|
+
* attributes (BOFF-13524, in ScheduleSpecEditor). For a saved workflow it
|
|
9327
|
+
* also describes the live schedule: its state, next fire times and recent
|
|
9328
|
+
* ticks, with Trigger now, Backfill and Pause with a note.
|
|
8345
9329
|
*
|
|
8346
9330
|
* A schedule saved without a policy keeps the legacy behaviour (every fire
|
|
8347
9331
|
* starts a run); SKIP is only the default the panel suggests for a new
|
|
@@ -8377,6 +9361,7 @@ function secondsOf(value) {
|
|
|
8377
9361
|
}
|
|
8378
9362
|
function SchedulePanel({ state, writeField, workflowKey, triggerNodeId, canExecute, canUpdate }) {
|
|
8379
9363
|
const tr = useTr();
|
|
9364
|
+
const { searchAttributes: searchAttributesOn } = usePhaseCFlags();
|
|
8380
9365
|
const policy = overlapPolicyOf(state.overlapPolicy);
|
|
8381
9366
|
const catchup = secondsOf(state.catchupWindow);
|
|
8382
9367
|
const limited = typeof state.limitedActions === "number" ? state.limitedActions : null;
|
|
@@ -8404,306 +9389,315 @@ function SchedulePanel({ state, writeField, workflowKey, triggerNodeId, canExecu
|
|
|
8404
9389
|
return /* @__PURE__ */ jsxs("div", {
|
|
8405
9390
|
className: "space-y-5 p-4",
|
|
8406
9391
|
"data-testid": "schedule-panel",
|
|
8407
|
-
children: [
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
children: [
|
|
8417
|
-
/* @__PURE__ */ jsx(Label, {
|
|
8418
|
-
htmlFor: "schedule-overlap",
|
|
8419
|
-
children: tr("fluidgrids.builder.schedule.overlap", "When the previous run is still open")
|
|
8420
|
-
}),
|
|
8421
|
-
/* @__PURE__ */ jsxs(NativeSelect, {
|
|
8422
|
-
id: "schedule-overlap",
|
|
8423
|
-
value: policy ?? "",
|
|
8424
|
-
disabled: !canUpdate,
|
|
8425
|
-
onChange: (value) => writeField("overlapPolicy", overlapPolicyOf(value) ?? void 0),
|
|
8426
|
-
testId: "schedule-overlap",
|
|
8427
|
-
children: [
|
|
8428
|
-
/* @__PURE__ */ jsx("option", {
|
|
8429
|
-
value: "",
|
|
8430
|
-
children: tr("fluidgrids.builder.schedule.overlapLegacy", "Start every run (no policy)")
|
|
8431
|
-
}),
|
|
8432
|
-
/* @__PURE__ */ jsx("option", {
|
|
8433
|
-
value: "SKIP",
|
|
8434
|
-
children: tr("fluidgrids.builder.schedule.skip", "Skip the fire")
|
|
8435
|
-
}),
|
|
8436
|
-
/* @__PURE__ */ jsx("option", {
|
|
8437
|
-
value: "BUFFER_ONE",
|
|
8438
|
-
children: tr("fluidgrids.builder.schedule.bufferOne", "Run once after it ends")
|
|
8439
|
-
}),
|
|
8440
|
-
/* @__PURE__ */ jsx("option", {
|
|
8441
|
-
value: "BUFFER_ALL",
|
|
8442
|
-
children: tr("fluidgrids.builder.schedule.bufferAll", "Run every missed fire after it ends")
|
|
8443
|
-
}),
|
|
8444
|
-
/* @__PURE__ */ jsx("option", {
|
|
8445
|
-
value: "CANCEL_OTHER",
|
|
8446
|
-
children: tr("fluidgrids.builder.schedule.cancelOther", "Cancel it and start")
|
|
8447
|
-
}),
|
|
8448
|
-
/* @__PURE__ */ jsx("option", {
|
|
8449
|
-
value: "TERMINATE_OTHER",
|
|
8450
|
-
children: tr("fluidgrids.builder.schedule.terminateOther", "Terminate it and start")
|
|
8451
|
-
}),
|
|
8452
|
-
/* @__PURE__ */ jsx("option", {
|
|
8453
|
-
value: "ALLOW_ALL",
|
|
8454
|
-
children: tr("fluidgrids.builder.schedule.allowAll", "Start anyway")
|
|
8455
|
-
})
|
|
8456
|
-
]
|
|
8457
|
-
}),
|
|
8458
|
-
!policy && canUpdate && /* @__PURE__ */ jsx(Button$1, {
|
|
8459
|
-
size: "sm",
|
|
8460
|
-
variant: "link",
|
|
8461
|
-
className: "h-auto p-0 text-xs",
|
|
8462
|
-
onClick: () => writeField("overlapPolicy", "SKIP"),
|
|
8463
|
-
"data-testid": "schedule-use-policy",
|
|
8464
|
-
children: tr("fluidgrids.builder.schedule.usePolicy", "Use a policy (skip while a run is open)")
|
|
8465
|
-
})
|
|
8466
|
-
]
|
|
8467
|
-
}),
|
|
8468
|
-
/* @__PURE__ */ jsxs("div", {
|
|
8469
|
-
className: "grid gap-3 sm:grid-cols-2",
|
|
8470
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
9392
|
+
children: [
|
|
9393
|
+
/* @__PURE__ */ jsxs("section", {
|
|
9394
|
+
className: "space-y-3",
|
|
9395
|
+
children: [
|
|
9396
|
+
/* @__PURE__ */ jsx("h4", {
|
|
9397
|
+
className: "text-sm font-semibold text-foreground",
|
|
9398
|
+
children: tr("fluidgrids.builder.schedule.policy", "Policy")
|
|
9399
|
+
}),
|
|
9400
|
+
/* @__PURE__ */ jsxs("div", {
|
|
8471
9401
|
className: "space-y-1.5",
|
|
8472
|
-
children: [
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
9402
|
+
children: [
|
|
9403
|
+
/* @__PURE__ */ jsx(Label, {
|
|
9404
|
+
htmlFor: "schedule-overlap",
|
|
9405
|
+
children: tr("fluidgrids.builder.schedule.overlap", "When the previous run is still open")
|
|
9406
|
+
}),
|
|
9407
|
+
/* @__PURE__ */ jsxs(NativeSelect, {
|
|
9408
|
+
id: "schedule-overlap",
|
|
9409
|
+
value: policy ?? "",
|
|
9410
|
+
disabled: !canUpdate,
|
|
9411
|
+
onChange: (value) => writeField("overlapPolicy", overlapPolicyOf(value) ?? void 0),
|
|
9412
|
+
testId: "schedule-overlap",
|
|
9413
|
+
children: [
|
|
9414
|
+
/* @__PURE__ */ jsx("option", {
|
|
9415
|
+
value: "",
|
|
9416
|
+
children: tr("fluidgrids.builder.schedule.overlapLegacy", "Start every run (no policy)")
|
|
9417
|
+
}),
|
|
9418
|
+
/* @__PURE__ */ jsx("option", {
|
|
9419
|
+
value: "SKIP",
|
|
9420
|
+
children: tr("fluidgrids.builder.schedule.skip", "Skip the fire")
|
|
9421
|
+
}),
|
|
9422
|
+
/* @__PURE__ */ jsx("option", {
|
|
9423
|
+
value: "BUFFER_ONE",
|
|
9424
|
+
children: tr("fluidgrids.builder.schedule.bufferOne", "Run once after it ends")
|
|
9425
|
+
}),
|
|
9426
|
+
/* @__PURE__ */ jsx("option", {
|
|
9427
|
+
value: "BUFFER_ALL",
|
|
9428
|
+
children: tr("fluidgrids.builder.schedule.bufferAll", "Run every missed fire after it ends")
|
|
9429
|
+
}),
|
|
9430
|
+
/* @__PURE__ */ jsx("option", {
|
|
9431
|
+
value: "CANCEL_OTHER",
|
|
9432
|
+
children: tr("fluidgrids.builder.schedule.cancelOther", "Cancel it and start")
|
|
9433
|
+
}),
|
|
9434
|
+
/* @__PURE__ */ jsx("option", {
|
|
9435
|
+
value: "TERMINATE_OTHER",
|
|
9436
|
+
children: tr("fluidgrids.builder.schedule.terminateOther", "Terminate it and start")
|
|
9437
|
+
}),
|
|
9438
|
+
/* @__PURE__ */ jsx("option", {
|
|
9439
|
+
value: "ALLOW_ALL",
|
|
9440
|
+
children: tr("fluidgrids.builder.schedule.allowAll", "Start anyway")
|
|
9441
|
+
})
|
|
9442
|
+
]
|
|
9443
|
+
}),
|
|
9444
|
+
!policy && canUpdate && /* @__PURE__ */ jsx(Button$1, {
|
|
9445
|
+
size: "sm",
|
|
9446
|
+
variant: "link",
|
|
9447
|
+
className: "h-auto p-0 text-xs",
|
|
9448
|
+
onClick: () => writeField("overlapPolicy", "SKIP"),
|
|
9449
|
+
"data-testid": "schedule-use-policy",
|
|
9450
|
+
children: tr("fluidgrids.builder.schedule.usePolicy", "Use a policy (skip while a run is open)")
|
|
9451
|
+
})
|
|
9452
|
+
]
|
|
9453
|
+
}),
|
|
9454
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9455
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
9456
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
9457
|
+
className: "space-y-1.5",
|
|
9458
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
9459
|
+
htmlFor: "schedule-catchup",
|
|
9460
|
+
children: tr("fluidgrids.builder.schedule.catchup", "Catch-up window (seconds)")
|
|
9461
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
9462
|
+
id: "schedule-catchup",
|
|
9463
|
+
inputMode: "numeric",
|
|
9464
|
+
disabled: !canUpdate,
|
|
9465
|
+
value: catchup === null ? "" : String(catchup),
|
|
9466
|
+
onChange: (event) => {
|
|
9467
|
+
const value = event.target.value.trim();
|
|
9468
|
+
writeField("catchupWindow", value === "" ? void 0 : /^\d+$/.test(value) ? Math.min(Number(value), 604800) : state.catchupWindow);
|
|
9469
|
+
},
|
|
9470
|
+
"data-testid": "schedule-catchup"
|
|
9471
|
+
})]
|
|
9472
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
9473
|
+
className: "space-y-1.5",
|
|
9474
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
9475
|
+
htmlFor: "schedule-limited",
|
|
9476
|
+
children: tr("fluidgrids.builder.schedule.limited", "Stop after this many runs (optional)")
|
|
9477
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
9478
|
+
id: "schedule-limited",
|
|
9479
|
+
inputMode: "numeric",
|
|
9480
|
+
disabled: !canUpdate,
|
|
9481
|
+
value: limited === null ? "" : String(limited),
|
|
9482
|
+
onChange: (event) => {
|
|
9483
|
+
const value = event.target.value.trim();
|
|
9484
|
+
writeField("limitedActions", value === "" ? void 0 : /^\d+$/.test(value) ? Number(value) : state.limitedActions);
|
|
9485
|
+
},
|
|
9486
|
+
"data-testid": "schedule-limited"
|
|
9487
|
+
})]
|
|
8485
9488
|
})]
|
|
8486
|
-
}),
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
8492
|
-
id: "schedule-limited",
|
|
8493
|
-
inputMode: "numeric",
|
|
9489
|
+
}),
|
|
9490
|
+
/* @__PURE__ */ jsxs("label", {
|
|
9491
|
+
className: "flex items-center justify-between gap-3 text-sm text-foreground",
|
|
9492
|
+
children: [tr("fluidgrids.builder.schedule.pauseOnFailure", "Pause the schedule when a run it started fails"), /* @__PURE__ */ jsx(Switch, {
|
|
9493
|
+
checked: state.pauseOnFailure === true,
|
|
8494
9494
|
disabled: !canUpdate,
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
const value = event.target.value.trim();
|
|
8498
|
-
writeField("limitedActions", value === "" ? void 0 : /^\d+$/.test(value) ? Number(value) : state.limitedActions);
|
|
8499
|
-
},
|
|
8500
|
-
"data-testid": "schedule-limited"
|
|
9495
|
+
onCheckedChange: (checked) => writeField("pauseOnFailure", checked ? true : void 0),
|
|
9496
|
+
"data-testid": "schedule-pause-on-failure"
|
|
8501
9497
|
})]
|
|
8502
|
-
})
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
icon: "mdi:refresh",
|
|
8544
|
-
width: 14
|
|
9498
|
+
}),
|
|
9499
|
+
/* @__PURE__ */ jsx("p", {
|
|
9500
|
+
className: "text-xs text-muted-foreground",
|
|
9501
|
+
children: tr("fluidgrids.builder.schedule.saveHint", "Policy changes apply when the workflow is saved and activated.")
|
|
9502
|
+
})
|
|
9503
|
+
]
|
|
9504
|
+
}),
|
|
9505
|
+
/* @__PURE__ */ jsx(ScheduleSpecEditor, {
|
|
9506
|
+
state,
|
|
9507
|
+
writeField,
|
|
9508
|
+
canUpdate,
|
|
9509
|
+
searchAttributesOn
|
|
9510
|
+
}),
|
|
9511
|
+
!workflowKey ? /* @__PURE__ */ jsx("p", {
|
|
9512
|
+
className: "text-sm text-muted-foreground",
|
|
9513
|
+
children: tr("fluidgrids.builder.schedule.unsaved", "Save the workflow to see and control its schedule.")
|
|
9514
|
+
}) : /* @__PURE__ */ jsxs("section", {
|
|
9515
|
+
className: "space-y-3",
|
|
9516
|
+
"data-testid": "schedule-live",
|
|
9517
|
+
children: [
|
|
9518
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9519
|
+
className: "flex flex-wrap items-center gap-2",
|
|
9520
|
+
children: [
|
|
9521
|
+
/* @__PURE__ */ jsx("h4", {
|
|
9522
|
+
className: "text-sm font-semibold text-foreground",
|
|
9523
|
+
children: tr("fluidgrids.builder.schedule.live", "Schedule")
|
|
9524
|
+
}),
|
|
9525
|
+
live && /* @__PURE__ */ jsx(StatusPill, {
|
|
9526
|
+
tone: live.state.paused ? "warning" : "success",
|
|
9527
|
+
testId: "schedule-state",
|
|
9528
|
+
children: live.state.paused ? tr("fluidgrids.builder.schedule.paused", "Paused") : tr("fluidgrids.builder.schedule.active", "Active")
|
|
9529
|
+
}),
|
|
9530
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
9531
|
+
size: "sm",
|
|
9532
|
+
variant: "ghost",
|
|
9533
|
+
className: "ml-auto",
|
|
9534
|
+
onClick: () => void describe.refetch(),
|
|
9535
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
9536
|
+
icon: "mdi:refresh",
|
|
9537
|
+
width: 14
|
|
9538
|
+
})
|
|
8545
9539
|
})
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
9540
|
+
]
|
|
9541
|
+
}),
|
|
9542
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
9543
|
+
message: describe.error,
|
|
9544
|
+
testId: "schedule-describe-error"
|
|
9545
|
+
}),
|
|
9546
|
+
live?.state.paused && live.state.note && /* @__PURE__ */ jsx("p", {
|
|
9547
|
+
className: "text-sm text-foreground",
|
|
9548
|
+
"data-testid": "schedule-note",
|
|
9549
|
+
children: live.state.note
|
|
9550
|
+
}),
|
|
9551
|
+
live && /* @__PURE__ */ jsxs("div", {
|
|
9552
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
9553
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
9554
|
+
className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
9555
|
+
children: tr("fluidgrids.builder.schedule.next", "Next fire times")
|
|
9556
|
+
}), /* @__PURE__ */ jsxs("ul", {
|
|
9557
|
+
className: "space-y-0.5 font-mono text-xs text-foreground",
|
|
9558
|
+
"data-testid": "schedule-next",
|
|
9559
|
+
children: [live.nextFireTimes.slice(0, 5).map((time) => /* @__PURE__ */ jsx("li", { children: new Date(time).toLocaleString() }, time)), live.nextFireTimes.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
|
|
9560
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
9561
|
+
className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
9562
|
+
children: tr("fluidgrids.builder.schedule.recent", "Recent actions")
|
|
9563
|
+
}), /* @__PURE__ */ jsxs("ul", {
|
|
9564
|
+
className: "space-y-0.5 text-xs text-foreground",
|
|
9565
|
+
"data-testid": "schedule-recent",
|
|
9566
|
+
children: [live.recentActions.slice(0, 5).map((tick) => /* @__PURE__ */ jsxs("li", { children: [
|
|
9567
|
+
/* @__PURE__ */ jsx("span", {
|
|
9568
|
+
className: "font-mono",
|
|
9569
|
+
children: new Date(tick.dueAt).toLocaleString()
|
|
9570
|
+
}),
|
|
9571
|
+
" ",
|
|
9572
|
+
"· ",
|
|
9573
|
+
tick.kind,
|
|
9574
|
+
" · ",
|
|
9575
|
+
tick.outcome
|
|
9576
|
+
] }, tick.id)), live.recentActions.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
|
|
9577
|
+
})] })]
|
|
9578
|
+
}),
|
|
9579
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9580
|
+
className: "flex flex-wrap gap-2",
|
|
9581
|
+
children: [
|
|
9582
|
+
canExecute && /* @__PURE__ */ jsxs(Button$1, {
|
|
9583
|
+
size: "sm",
|
|
9584
|
+
variant: "outline",
|
|
9585
|
+
disabled: busy !== null,
|
|
9586
|
+
onClick: () => void act("trigger", async () => {
|
|
9587
|
+
const result = await triggerSchedule({
|
|
9588
|
+
workflowKey,
|
|
9589
|
+
triggerNodeId
|
|
9590
|
+
});
|
|
9591
|
+
toast.success(result.runId ? tr("fluidgrids.builder.schedule.triggered", "Run started") : tr("fluidgrids.builder.schedule.triggeredOutcome", "Tick recorded: {{outcome}}", { outcome: result.tick.outcome }));
|
|
9592
|
+
await describe.refetch();
|
|
9593
|
+
}),
|
|
9594
|
+
"data-testid": "schedule-trigger",
|
|
9595
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
9596
|
+
icon: "mdi:play",
|
|
9597
|
+
className: "mr-1",
|
|
9598
|
+
width: 14
|
|
9599
|
+
}), tr("fluidgrids.builder.schedule.trigger", "Trigger now")]
|
|
8577
9600
|
}),
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
9601
|
+
canUpdate && live && !live.state.paused && /* @__PURE__ */ jsxs(Button$1, {
|
|
9602
|
+
size: "sm",
|
|
9603
|
+
variant: "outline",
|
|
9604
|
+
disabled: busy !== null,
|
|
9605
|
+
onClick: () => void act("pause", async () => {
|
|
9606
|
+
describe.set(await pauseSchedule({
|
|
9607
|
+
workflowKey,
|
|
9608
|
+
triggerNodeId,
|
|
9609
|
+
note
|
|
9610
|
+
}));
|
|
9611
|
+
setNote("");
|
|
9612
|
+
}),
|
|
9613
|
+
"data-testid": "schedule-pause",
|
|
9614
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
9615
|
+
icon: "mdi:pause",
|
|
9616
|
+
className: "mr-1",
|
|
9617
|
+
width: 14
|
|
9618
|
+
}), tr("fluidgrids.builder.schedule.pause", "Pause")]
|
|
9619
|
+
}),
|
|
9620
|
+
canUpdate && live?.state.paused && /* @__PURE__ */ jsxs(Button$1, {
|
|
9621
|
+
size: "sm",
|
|
9622
|
+
variant: "outline",
|
|
9623
|
+
disabled: busy !== null,
|
|
9624
|
+
onClick: () => void act("unpause", async () => describe.set(await unpauseSchedule({
|
|
8595
9625
|
workflowKey,
|
|
8596
9626
|
triggerNodeId
|
|
8597
|
-
})
|
|
8598
|
-
|
|
8599
|
-
|
|
9627
|
+
}))),
|
|
9628
|
+
"data-testid": "schedule-unpause",
|
|
9629
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
9630
|
+
icon: "mdi:play-pause",
|
|
9631
|
+
className: "mr-1",
|
|
9632
|
+
width: 14
|
|
9633
|
+
}), tr("fluidgrids.builder.schedule.unpause", "Unpause")]
|
|
9634
|
+
})
|
|
9635
|
+
]
|
|
9636
|
+
}),
|
|
9637
|
+
canUpdate && live && !live.state.paused && /* @__PURE__ */ jsx(Input, {
|
|
9638
|
+
value: note,
|
|
9639
|
+
maxLength: 1e3,
|
|
9640
|
+
placeholder: tr("fluidgrids.builder.schedule.notePlaceholder", "Note for the pause (optional)"),
|
|
9641
|
+
onChange: (event) => setNote(event.target.value),
|
|
9642
|
+
"data-testid": "schedule-pause-note"
|
|
9643
|
+
}),
|
|
9644
|
+
canExecute && /* @__PURE__ */ jsxs("div", {
|
|
9645
|
+
className: "space-y-2 rounded-md border border-border p-3",
|
|
9646
|
+
children: [
|
|
9647
|
+
/* @__PURE__ */ jsx("p", {
|
|
9648
|
+
className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
9649
|
+
children: tr("fluidgrids.builder.schedule.backfill", "Backfill")
|
|
8600
9650
|
}),
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
note
|
|
8617
|
-
}));
|
|
8618
|
-
setNote("");
|
|
9651
|
+
/* @__PURE__ */ jsxs("div", {
|
|
9652
|
+
className: "grid gap-2 sm:grid-cols-2",
|
|
9653
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
9654
|
+
type: "datetime-local",
|
|
9655
|
+
value: from,
|
|
9656
|
+
onChange: (event) => setFrom(event.target.value),
|
|
9657
|
+
"aria-label": tr("fluidgrids.builder.schedule.from", "From"),
|
|
9658
|
+
"data-testid": "schedule-backfill-from"
|
|
9659
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
9660
|
+
type: "datetime-local",
|
|
9661
|
+
value: to,
|
|
9662
|
+
onChange: (event) => setTo(event.target.value),
|
|
9663
|
+
"aria-label": tr("fluidgrids.builder.schedule.to", "To"),
|
|
9664
|
+
"data-testid": "schedule-backfill-to"
|
|
9665
|
+
})]
|
|
8619
9666
|
}),
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
}), tr("fluidgrids.builder.schedule.unpause", "Unpause")]
|
|
8641
|
-
})
|
|
8642
|
-
]
|
|
8643
|
-
}),
|
|
8644
|
-
canUpdate && live && !live.state.paused && /* @__PURE__ */ jsx(Input, {
|
|
8645
|
-
value: note,
|
|
8646
|
-
maxLength: 1e3,
|
|
8647
|
-
placeholder: tr("fluidgrids.builder.schedule.notePlaceholder", "Note for the pause (optional)"),
|
|
8648
|
-
onChange: (event) => setNote(event.target.value),
|
|
8649
|
-
"data-testid": "schedule-pause-note"
|
|
8650
|
-
}),
|
|
8651
|
-
canExecute && /* @__PURE__ */ jsxs("div", {
|
|
8652
|
-
className: "space-y-2 rounded-md border border-border p-3",
|
|
8653
|
-
children: [
|
|
8654
|
-
/* @__PURE__ */ jsx("p", {
|
|
8655
|
-
className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
8656
|
-
children: tr("fluidgrids.builder.schedule.backfill", "Backfill")
|
|
8657
|
-
}),
|
|
8658
|
-
/* @__PURE__ */ jsxs("div", {
|
|
8659
|
-
className: "grid gap-2 sm:grid-cols-2",
|
|
8660
|
-
children: [/* @__PURE__ */ jsx(Input, {
|
|
8661
|
-
type: "datetime-local",
|
|
8662
|
-
value: from,
|
|
8663
|
-
onChange: (event) => setFrom(event.target.value),
|
|
8664
|
-
"aria-label": tr("fluidgrids.builder.schedule.from", "From"),
|
|
8665
|
-
"data-testid": "schedule-backfill-from"
|
|
8666
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
8667
|
-
type: "datetime-local",
|
|
8668
|
-
value: to,
|
|
8669
|
-
onChange: (event) => setTo(event.target.value),
|
|
8670
|
-
"aria-label": tr("fluidgrids.builder.schedule.to", "To"),
|
|
8671
|
-
"data-testid": "schedule-backfill-to"
|
|
8672
|
-
})]
|
|
8673
|
-
}),
|
|
8674
|
-
/* @__PURE__ */ jsx(Button$1, {
|
|
8675
|
-
size: "sm",
|
|
8676
|
-
variant: "outline",
|
|
8677
|
-
disabled: !backfillValid || busy !== null,
|
|
8678
|
-
onClick: () => void act("backfill", async () => {
|
|
8679
|
-
if (!backfillFrom || !backfillTo) return;
|
|
8680
|
-
const result = await backfillSchedule({
|
|
8681
|
-
workflowKey,
|
|
8682
|
-
triggerNodeId,
|
|
8683
|
-
from: backfillFrom.toISOString(),
|
|
8684
|
-
to: backfillTo.toISOString()
|
|
8685
|
-
});
|
|
8686
|
-
toast.success(tr("fluidgrids.builder.schedule.backfilled", "{{queued}} of {{fireTimes}} fire times queued", {
|
|
8687
|
-
queued: result.queued,
|
|
8688
|
-
fireTimes: result.fireTimes
|
|
8689
|
-
}));
|
|
8690
|
-
await describe.refetch();
|
|
9667
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
9668
|
+
size: "sm",
|
|
9669
|
+
variant: "outline",
|
|
9670
|
+
disabled: !backfillValid || busy !== null,
|
|
9671
|
+
onClick: () => void act("backfill", async () => {
|
|
9672
|
+
if (!backfillFrom || !backfillTo) return;
|
|
9673
|
+
const result = await backfillSchedule({
|
|
9674
|
+
workflowKey,
|
|
9675
|
+
triggerNodeId,
|
|
9676
|
+
from: backfillFrom.toISOString(),
|
|
9677
|
+
to: backfillTo.toISOString()
|
|
9678
|
+
});
|
|
9679
|
+
toast.success(tr("fluidgrids.builder.schedule.backfilled", "{{queued}} of {{fireTimes}} fire times queued", {
|
|
9680
|
+
queued: result.queued,
|
|
9681
|
+
fireTimes: result.fireTimes
|
|
9682
|
+
}));
|
|
9683
|
+
await describe.refetch();
|
|
9684
|
+
}),
|
|
9685
|
+
"data-testid": "schedule-backfill",
|
|
9686
|
+
children: tr("fluidgrids.builder.schedule.runBackfill", "Run the fire times in this range")
|
|
8691
9687
|
}),
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
]
|
|
8706
|
-
})]
|
|
9688
|
+
/* @__PURE__ */ jsx("p", {
|
|
9689
|
+
className: "text-xs text-muted-foreground",
|
|
9690
|
+
children: tr("fluidgrids.builder.schedule.backfillHint", "At most {{max}} past fire times; each started run counts toward the monthly runs.", { max: 1e3 })
|
|
9691
|
+
})
|
|
9692
|
+
]
|
|
9693
|
+
}),
|
|
9694
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
9695
|
+
message: error,
|
|
9696
|
+
testId: "schedule-error"
|
|
9697
|
+
})
|
|
9698
|
+
]
|
|
9699
|
+
})
|
|
9700
|
+
]
|
|
8707
9701
|
});
|
|
8708
9702
|
}
|
|
8709
9703
|
|