@parallelworks/ui 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ai/components/ChatInput.js +8 -23
- package/dist/ai/components/chatDraft.d.ts +7 -0
- package/dist/ai/components/chatDraft.js +66 -0
- package/dist/editing.d.ts +2 -0
- package/dist/editor/Monaco.js +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/lintContext.js +15 -3
- package/dist/graph/DependencyGraph.d.ts +8 -1
- package/dist/graph/DependencyGraph.js +433 -204
- package/dist/graph/GraphEditor.d.ts +27 -0
- package/dist/graph/GraphEditor.js +1543 -0
- package/dist/graph/GraphEditorDialogs.d.ts +87 -0
- package/dist/graph/GraphEditorDialogs.js +1808 -0
- package/dist/graph/InputDialog.js +4 -4
- package/dist/graph/InputsEditor.js +2 -2
- package/dist/graph/JobSummary.d.ts +3 -0
- package/dist/graph/JobSummary.js +34 -21
- package/dist/graph/MatrixGroup.d.ts +2 -1
- package/dist/graph/MatrixGroup.js +10 -3
- package/dist/graph/WorkflowSettingsDialog.d.ts +21 -0
- package/dist/graph/WorkflowSettingsDialog.js +670 -0
- package/dist/graph/editorFields.js +144 -3
- package/dist/graph/editorRows.d.ts +58 -0
- package/dist/graph/editorRows.js +460 -0
- package/dist/graph/editorStrings.d.ts +1 -0
- package/dist/graph/editorStrings.js +1 -0
- package/dist/graph/expressionRefs.js +39 -0
- package/dist/graph/fieldProblems.js +36 -2
- package/dist/graph/graphEditorState.d.ts +191 -0
- package/dist/graph/graphEditorState.js +74 -0
- package/dist/graph/index.d.ts +3 -0
- package/dist/graph/index.js +5 -2
- package/dist/graph/inputLayout.js +1 -1
- package/dist/graph/inputRefs.js +196 -4
- package/dist/graph/refusalText.js +13 -0
- package/dist/graph/stepWith.d.ts +38 -0
- package/dist/graph/stepWith.js +536 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1808 @@
|
|
|
1
|
+
import { useWorkflowActions, useWorkflowEditing } from "../components/Provider.js";
|
|
2
|
+
import { BiSolidTrashAlt } from "../icons/bi/BiSolidTrashAlt.js";
|
|
3
|
+
import { FaArrowDown } from "../icons/fa/FaArrowDown.js";
|
|
4
|
+
import { FaArrowUp } from "../icons/fa/FaArrowUp.js";
|
|
5
|
+
import { IoMdClose } from "../icons/io/IoMdClose.js";
|
|
6
|
+
import { withPositionKeys } from "../components/keys.js";
|
|
7
|
+
import { IconButton } from "../components/IconButton.js";
|
|
8
|
+
import { Input, fieldBoxClasses } from "../components/Input.js";
|
|
9
|
+
import { useGraphEditorStrings } from "./editorStrings.js";
|
|
10
|
+
import { asRecord } from "./records.js";
|
|
11
|
+
import { useLintContext, useLintEditing } from "../editor/lintContext.js";
|
|
12
|
+
import { JOB_YAML_PATH, STEP_YAML_PATH } from "../editor/settingsYaml.js";
|
|
13
|
+
import { FieldProblems, LintScope, ProblemList, useFieldProblems } from "./fieldProblems.js";
|
|
14
|
+
import { AddRowButton, ChoiceButtons, DialogShell, ENV_KEY, EXPRESSION, ExpressionToggle, FieldError, FieldLabel, KeyValueEditor, LabelledField, Section, SectionMemory, ToggleField, conditionText, countError, diffPatch, durationError, expressionError, flagError, flagOf, isEmptyPatch, isScalar, labelled, orUndefined, parseCondition, parseCount, parseJson, parseScalar, removeAt, rowsError, rowsFrom, rowsTo, sameValue, text, unchangedValue, updateAt, useSectionMemory } from "./editorFields.js";
|
|
15
|
+
import { ConditionField, FlagField, InputRefSelect, ScriptField, ValueOrInputField, readRef, refExpression, refSuggestions } from "./inputRefs.js";
|
|
16
|
+
import { expressionRefs } from "./expressionRefs.js";
|
|
17
|
+
import { NO_SCOPED, ViewSwitch, YamlPane, yamlProblem } from "./settingsViews.js";
|
|
18
|
+
import { useNewInputs } from "./InputDialog.js";
|
|
19
|
+
import { editErrorText } from "./refusalText.js";
|
|
20
|
+
import { UsesPicker, WithEditor, usesKind, withDraftFrom, withError, withValue } from "./stepWith.js";
|
|
21
|
+
import cx from "classnames";
|
|
22
|
+
import { useId, useMemo, useRef, useState } from "react";
|
|
23
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
24
|
+
//#region src/graph/GraphEditorDialogs.tsx
|
|
25
|
+
var SHELL = /^$|^(\/?([^/ ]+\/)*)?(bash|sh)( .*)?$/;
|
|
26
|
+
var OUTPUT_KEY = /^[a-zA-Z0-9_-]+$/;
|
|
27
|
+
var MATRIX_KEY = /^[a-zA-Z0-9_-]+$/;
|
|
28
|
+
var SSH_KEYS = [
|
|
29
|
+
"remoteHost",
|
|
30
|
+
"remoteUser",
|
|
31
|
+
"jumpNodeHost",
|
|
32
|
+
"jumpNodeUser",
|
|
33
|
+
"disconnect-timeout"
|
|
34
|
+
];
|
|
35
|
+
/** What the job dialog writes; the schema coverage test holds these to the workflow schema. */
|
|
36
|
+
var JOB_FIELDS = [
|
|
37
|
+
"needs",
|
|
38
|
+
"if",
|
|
39
|
+
"ssh",
|
|
40
|
+
"runs-on",
|
|
41
|
+
"working-directory",
|
|
42
|
+
"timeout",
|
|
43
|
+
"env",
|
|
44
|
+
"outputs",
|
|
45
|
+
"strategy"
|
|
46
|
+
];
|
|
47
|
+
var RUNS_ON_FIELDS = [
|
|
48
|
+
"environment",
|
|
49
|
+
"environment.cluster",
|
|
50
|
+
"environment.name",
|
|
51
|
+
"params",
|
|
52
|
+
"mode",
|
|
53
|
+
"targetId",
|
|
54
|
+
"environmentId",
|
|
55
|
+
"schedulingParams",
|
|
56
|
+
"newWorker",
|
|
57
|
+
"workerId"
|
|
58
|
+
];
|
|
59
|
+
var STRATEGY_FIELDS = [
|
|
60
|
+
"matrix",
|
|
61
|
+
"matrix.include",
|
|
62
|
+
"matrix.exclude",
|
|
63
|
+
"fail-fast",
|
|
64
|
+
"max-parallel"
|
|
65
|
+
];
|
|
66
|
+
/** What the step dialog writes. */
|
|
67
|
+
var STEP_FIELDS = [
|
|
68
|
+
"name",
|
|
69
|
+
"run",
|
|
70
|
+
"shell",
|
|
71
|
+
"uses",
|
|
72
|
+
"with",
|
|
73
|
+
"if",
|
|
74
|
+
"working-directory",
|
|
75
|
+
"id",
|
|
76
|
+
"timeout",
|
|
77
|
+
"ignore-errors",
|
|
78
|
+
"early-cancel",
|
|
79
|
+
"cleanup",
|
|
80
|
+
"retry",
|
|
81
|
+
"env",
|
|
82
|
+
"ssh"
|
|
83
|
+
];
|
|
84
|
+
var RETRY_FIELDS = [
|
|
85
|
+
"max-retries",
|
|
86
|
+
"interval",
|
|
87
|
+
"timeout",
|
|
88
|
+
"clean-on-retry"
|
|
89
|
+
];
|
|
90
|
+
var CLUSTER_TYPES = ["compute-clusters", "compute-resources"];
|
|
91
|
+
function sshFrom(value) {
|
|
92
|
+
const record = asRecord(value);
|
|
93
|
+
return Object.fromEntries(SSH_KEYS.map((key) => [key, text(record[key])]));
|
|
94
|
+
}
|
|
95
|
+
function sshTo(fields, original) {
|
|
96
|
+
const written = asRecord(original);
|
|
97
|
+
const entries = SSH_KEYS.flatMap((key) => {
|
|
98
|
+
const value = fields[key].trim();
|
|
99
|
+
return value ? [[key, unchangedValue(value, written[key]) ? written[key] : value]] : [];
|
|
100
|
+
});
|
|
101
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
102
|
+
}
|
|
103
|
+
function sshError(fields, t) {
|
|
104
|
+
return SSH_KEYS.some((key) => fields[key].trim()) && !fields.remoteHost.trim() ? t.remoteHostRequired : void 0;
|
|
105
|
+
}
|
|
106
|
+
var TARGET_MODES = [
|
|
107
|
+
"login",
|
|
108
|
+
"environment",
|
|
109
|
+
"worker"
|
|
110
|
+
];
|
|
111
|
+
var TARGET_KEYS = [
|
|
112
|
+
"mode",
|
|
113
|
+
"targetId",
|
|
114
|
+
"environmentId",
|
|
115
|
+
"schedulingParams",
|
|
116
|
+
"newWorker",
|
|
117
|
+
"workerId"
|
|
118
|
+
];
|
|
119
|
+
function runsOnFrom(value) {
|
|
120
|
+
const record = asRecord(value);
|
|
121
|
+
const environment = asRecord(record["environment"]);
|
|
122
|
+
const newWorker = record["newWorker"];
|
|
123
|
+
return {
|
|
124
|
+
kind: TARGET_KEYS.some((key) => key in record) ? "target" : "name",
|
|
125
|
+
cluster: text(environment["cluster"]),
|
|
126
|
+
name: text(environment["name"]),
|
|
127
|
+
params: rowsFrom(record["params"], false),
|
|
128
|
+
mode: text(record["mode"]),
|
|
129
|
+
targetId: text(record["targetId"]),
|
|
130
|
+
environmentId: text(record["environmentId"]),
|
|
131
|
+
schedulingParams: rowsFrom(record["schedulingParams"], false),
|
|
132
|
+
newWorker: typeof newWorker === "boolean" ? newWorker : void 0,
|
|
133
|
+
workerId: text(record["workerId"])
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function runsOnTo(fields) {
|
|
137
|
+
if (fields.kind === "target") {
|
|
138
|
+
const mode = fields.mode.trim();
|
|
139
|
+
const targetId = fields.targetId.trim();
|
|
140
|
+
const environmentId = fields.environmentId.trim();
|
|
141
|
+
const schedulingParams = rowsTo(fields.schedulingParams, parseScalar);
|
|
142
|
+
const workerId = fields.workerId.trim();
|
|
143
|
+
if (!mode && !targetId && !environmentId && !schedulingParams && fields.newWorker === void 0 && !workerId) return;
|
|
144
|
+
return {
|
|
145
|
+
mode,
|
|
146
|
+
...targetId ? { targetId } : {},
|
|
147
|
+
...environmentId ? { environmentId } : {},
|
|
148
|
+
...schedulingParams ? { schedulingParams } : {},
|
|
149
|
+
...fields.newWorker === void 0 ? {} : { newWorker: fields.newWorker },
|
|
150
|
+
...workerId ? { workerId } : {}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const cluster = fields.cluster.trim();
|
|
154
|
+
const name = fields.name.trim();
|
|
155
|
+
const params = rowsTo(fields.params, parseScalar);
|
|
156
|
+
if (!cluster && !name && !params) return;
|
|
157
|
+
return {
|
|
158
|
+
environment: {
|
|
159
|
+
cluster,
|
|
160
|
+
name
|
|
161
|
+
},
|
|
162
|
+
...params ? { params } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
var PARAM_KEY = /^[A-Za-z0-9_-]+$/;
|
|
166
|
+
function runsOnError(fields, sshUsed, t) {
|
|
167
|
+
if (!runsOnTo(fields)) return;
|
|
168
|
+
if (sshUsed) return t.runsOnWithSsh;
|
|
169
|
+
if (fields.kind === "target") {
|
|
170
|
+
if (!TARGET_MODES.includes(fields.mode.trim())) return t.runsOnNeedsMode;
|
|
171
|
+
return rowsError(fields.schedulingParams, PARAM_KEY, t.invalidKey, t);
|
|
172
|
+
}
|
|
173
|
+
if (!fields.cluster.trim() || !fields.name.trim()) return t.runsOnNeedsEnvironment;
|
|
174
|
+
return rowsError(fields.params, PARAM_KEY, t.invalidKey, t);
|
|
175
|
+
}
|
|
176
|
+
function sshHelp(key, t) {
|
|
177
|
+
return key === "disconnect-timeout" ? t.help.disconnectTimeout : t.help[key];
|
|
178
|
+
}
|
|
179
|
+
function sshLabel(key, t) {
|
|
180
|
+
return key === "disconnect-timeout" ? t.fields.disconnectTimeout : t.fields[key];
|
|
181
|
+
}
|
|
182
|
+
function SshEditor({ fields, onChange, error, source }) {
|
|
183
|
+
const t = useGraphEditorStrings();
|
|
184
|
+
const set = (key) => (value) => onChange({
|
|
185
|
+
...fields,
|
|
186
|
+
[key]: value
|
|
187
|
+
});
|
|
188
|
+
return /* @__PURE__ */ jsxs(LintScope, {
|
|
189
|
+
at: "ssh",
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ jsx(ValueOrInputField, {
|
|
192
|
+
label: t.fields.remoteHost,
|
|
193
|
+
yamlKey: "remoteHost",
|
|
194
|
+
description: t.help.remoteHost,
|
|
195
|
+
value: fields.remoteHost,
|
|
196
|
+
onChange: set("remoteHost"),
|
|
197
|
+
source,
|
|
198
|
+
types: CLUSTER_TYPES,
|
|
199
|
+
suffix: "ip"
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ jsx(ValueOrInputField, {
|
|
202
|
+
label: t.fields.remoteUser,
|
|
203
|
+
yamlKey: "remoteUser",
|
|
204
|
+
description: t.help.remoteUser,
|
|
205
|
+
value: fields.remoteUser,
|
|
206
|
+
onChange: set("remoteUser"),
|
|
207
|
+
source,
|
|
208
|
+
types: CLUSTER_TYPES,
|
|
209
|
+
suffix: "user"
|
|
210
|
+
}),
|
|
211
|
+
[
|
|
212
|
+
"jumpNodeHost",
|
|
213
|
+
"jumpNodeUser",
|
|
214
|
+
"disconnect-timeout"
|
|
215
|
+
].map((key) => /* @__PURE__ */ jsx(Input, {
|
|
216
|
+
mono: true,
|
|
217
|
+
...labelled(sshLabel(key, t), key),
|
|
218
|
+
description: sshHelp(key, t),
|
|
219
|
+
value: fields[key],
|
|
220
|
+
onChange: (e) => set(key)(e.target.value)
|
|
221
|
+
}, key)),
|
|
222
|
+
/* @__PURE__ */ jsx(FieldError, { message: error })
|
|
223
|
+
]
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** A job's or step's env, each value checked against what it reads. */
|
|
227
|
+
function EnvSection({ description, rows, onChange, error, source }) {
|
|
228
|
+
const t = useGraphEditorStrings();
|
|
229
|
+
return /* @__PURE__ */ jsx(Section, {
|
|
230
|
+
title: t.sectionEnvironment,
|
|
231
|
+
description,
|
|
232
|
+
open: rows.length > 0,
|
|
233
|
+
alert: !!error,
|
|
234
|
+
children: /* @__PURE__ */ jsx(LintScope, {
|
|
235
|
+
at: "env",
|
|
236
|
+
children: /* @__PURE__ */ jsx(KeyValueEditor, {
|
|
237
|
+
lint: true,
|
|
238
|
+
rows,
|
|
239
|
+
onChange,
|
|
240
|
+
error,
|
|
241
|
+
valueSuggestions: refSuggestions(source)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
var OUTPUT_REF = /^\$\{\{\s*needs\.([A-Za-z0-9_-]+)\.steps\.([A-Za-z0-9_-]+)\.outputs\.([A-Za-z0-9_-]+)\s*\}\}$/;
|
|
247
|
+
function outputRowsFrom(value, job) {
|
|
248
|
+
return Object.entries(asRecord(value)).map(([name, raw]) => {
|
|
249
|
+
const match = typeof raw === "string" ? OUTPUT_REF.exec(raw.trim()) : null;
|
|
250
|
+
return match && match[1] === job ? {
|
|
251
|
+
name,
|
|
252
|
+
step: match[2] ?? "",
|
|
253
|
+
output: match[3] ?? "",
|
|
254
|
+
original: String(raw)
|
|
255
|
+
} : {
|
|
256
|
+
name,
|
|
257
|
+
step: "",
|
|
258
|
+
output: "",
|
|
259
|
+
custom: text(raw)
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function outputsTo(rows, job) {
|
|
264
|
+
if (rows.length === 0) return;
|
|
265
|
+
return Object.fromEntries(rows.map((row) => {
|
|
266
|
+
const written = `\${{ needs.${job}.steps.${row.step}.outputs.${row.output.trim()} }}`;
|
|
267
|
+
const same = row.original?.replace(/\s/g, "") === written.replace(/\s/g, "");
|
|
268
|
+
return [row.name.trim(), row.custom ?? (same ? row.original : written)];
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
function outputsError(rows, t) {
|
|
272
|
+
const names = rows.map((row) => row.name.trim());
|
|
273
|
+
for (const [i, row] of rows.entries()) {
|
|
274
|
+
if (!OUTPUT_KEY.test(names[i] ?? "")) return t.invalidKey;
|
|
275
|
+
if (names.indexOf(names[i] ?? "") !== i) return t.duplicateKey;
|
|
276
|
+
if (row.custom !== void 0 && !row.custom.trim()) return t.required;
|
|
277
|
+
if (row.custom === void 0 && (!row.step || !OUTPUT_KEY.test(row.output.trim()))) return t.outputNeedsStep;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function OutputsEditor({ rows, onChange, stepIds, error }) {
|
|
281
|
+
const t = useGraphEditorStrings();
|
|
282
|
+
const update = (i, patch) => onChange(rows.map((row, j) => i === j ? {
|
|
283
|
+
...row,
|
|
284
|
+
...patch
|
|
285
|
+
} : row));
|
|
286
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
287
|
+
className: "flex flex-col gap-2",
|
|
288
|
+
children: [
|
|
289
|
+
stepIds.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
290
|
+
className: "text-xs theme-muted-text",
|
|
291
|
+
children: t.noStepIds
|
|
292
|
+
}),
|
|
293
|
+
withPositionKeys(rows).map(({ key, item: row }, i) => /* @__PURE__ */ jsxs("div", {
|
|
294
|
+
className: "flex items-center gap-2",
|
|
295
|
+
children: [
|
|
296
|
+
/* @__PURE__ */ jsx("div", {
|
|
297
|
+
className: "w-1/3",
|
|
298
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
299
|
+
mono: true,
|
|
300
|
+
"aria-label": t.outputName,
|
|
301
|
+
placeholder: t.outputName,
|
|
302
|
+
value: row.name,
|
|
303
|
+
onChange: (e) => update(i, { name: e.target.value })
|
|
304
|
+
})
|
|
305
|
+
}),
|
|
306
|
+
row.custom !== void 0 ? /* @__PURE__ */ jsx("div", {
|
|
307
|
+
className: "flex-1",
|
|
308
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
309
|
+
mono: true,
|
|
310
|
+
"aria-label": t.outputValue,
|
|
311
|
+
value: row.custom,
|
|
312
|
+
onChange: (e) => update(i, { custom: e.target.value })
|
|
313
|
+
})
|
|
314
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
315
|
+
className: "w-1/3",
|
|
316
|
+
children: /* @__PURE__ */ jsxs("select", {
|
|
317
|
+
"aria-label": t.outputStep,
|
|
318
|
+
value: row.step,
|
|
319
|
+
onChange: (e) => update(i, { step: e.target.value }),
|
|
320
|
+
className: fieldBoxClasses,
|
|
321
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
322
|
+
value: "",
|
|
323
|
+
disabled: true,
|
|
324
|
+
children: t.outputStep
|
|
325
|
+
}), stepIds.map((id) => /* @__PURE__ */ jsx("option", {
|
|
326
|
+
value: id,
|
|
327
|
+
children: id
|
|
328
|
+
}, id))]
|
|
329
|
+
})
|
|
330
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
331
|
+
className: "flex-1",
|
|
332
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
333
|
+
mono: true,
|
|
334
|
+
"aria-label": t.outputKey,
|
|
335
|
+
placeholder: t.outputKey,
|
|
336
|
+
value: row.output,
|
|
337
|
+
onChange: (e) => update(i, { output: e.target.value })
|
|
338
|
+
})
|
|
339
|
+
})] }),
|
|
340
|
+
/* @__PURE__ */ jsx(ExpressionToggle, {
|
|
341
|
+
active: row.custom !== void 0,
|
|
342
|
+
onChange: (on) => onChange(rows.map((other, j) => j === i ? on ? {
|
|
343
|
+
name: other.name,
|
|
344
|
+
step: "",
|
|
345
|
+
output: "",
|
|
346
|
+
custom: ""
|
|
347
|
+
} : {
|
|
348
|
+
name: other.name,
|
|
349
|
+
step: stepIds[0] ?? "",
|
|
350
|
+
output: ""
|
|
351
|
+
} : other))
|
|
352
|
+
}),
|
|
353
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
354
|
+
icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
|
|
355
|
+
label: t.removeRow,
|
|
356
|
+
variant: "ghost",
|
|
357
|
+
size: "sm",
|
|
358
|
+
onClick: () => onChange(removeAt(rows, i))
|
|
359
|
+
})
|
|
360
|
+
]
|
|
361
|
+
}, key)),
|
|
362
|
+
/* @__PURE__ */ jsx(FieldError, { message: error }),
|
|
363
|
+
/* @__PURE__ */ jsx(AddRowButton, {
|
|
364
|
+
label: t.addRow,
|
|
365
|
+
onClick: () => onChange([...rows, {
|
|
366
|
+
name: "",
|
|
367
|
+
step: stepIds[0] ?? "",
|
|
368
|
+
output: ""
|
|
369
|
+
}])
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
var matrixRowKeys = 0;
|
|
375
|
+
var matrixRow = (row) => ({
|
|
376
|
+
...row,
|
|
377
|
+
key: ++matrixRowKeys
|
|
378
|
+
});
|
|
379
|
+
function matrixRowsFrom(matrix) {
|
|
380
|
+
return Object.entries(asRecord(matrix)).filter(([name]) => name !== "include" && name !== "exclude").map(([name, values]) => {
|
|
381
|
+
if (typeof values === "string") return matrixRow({
|
|
382
|
+
name,
|
|
383
|
+
values,
|
|
384
|
+
original: values,
|
|
385
|
+
initial: values
|
|
386
|
+
});
|
|
387
|
+
if (Array.isArray(values) && values.every((value) => isScalar(value) && !(typeof value === "string" && value.includes(",")))) {
|
|
388
|
+
const joined = values.join(", ");
|
|
389
|
+
return matrixRow({
|
|
390
|
+
name,
|
|
391
|
+
values: joined,
|
|
392
|
+
original: values,
|
|
393
|
+
initial: joined
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const json = JSON.stringify(values);
|
|
397
|
+
return matrixRow({
|
|
398
|
+
name,
|
|
399
|
+
values: json,
|
|
400
|
+
nested: true,
|
|
401
|
+
original: values,
|
|
402
|
+
initial: json
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function matrixValues(row) {
|
|
407
|
+
if (row.initial !== void 0 && row.values === row.initial) return row.original;
|
|
408
|
+
const trimmed = row.values.trim();
|
|
409
|
+
if (row.nested) return parseJson(trimmed).value;
|
|
410
|
+
if (EXPRESSION.test(trimmed)) return trimmed;
|
|
411
|
+
return trimmed.split(",").map((value) => value.trim()).filter(Boolean).map(parseScalar);
|
|
412
|
+
}
|
|
413
|
+
function matrixRowInvalid(row, names, i) {
|
|
414
|
+
const name = names[i] ?? "";
|
|
415
|
+
const values = matrixValues(row);
|
|
416
|
+
return !MATRIX_KEY.test(name) || name === "include" || name === "exclude" || names.indexOf(name) !== i || !(typeof values === "string" || Array.isArray(values)) || Array.isArray(values) && values.length === 0;
|
|
417
|
+
}
|
|
418
|
+
function entriesFrom(value) {
|
|
419
|
+
return Array.isArray(value) ? value.map((entry) => rowsFrom(entry)) : [];
|
|
420
|
+
}
|
|
421
|
+
function entriesTo(entries) {
|
|
422
|
+
return entries.length > 0 ? entries.map((rows) => rowsTo(rows, parseScalar) ?? {}) : void 0;
|
|
423
|
+
}
|
|
424
|
+
function entriesError(entries, t) {
|
|
425
|
+
for (const rows of entries) {
|
|
426
|
+
const error = rows.length === 0 ? t.invalidMatrixEntry : rowsError(rows, MATRIX_KEY, t.invalidKey, t);
|
|
427
|
+
if (error) return error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/** Combinations to add or skip, as entries of keys and values or as one expression. */
|
|
431
|
+
function MatrixEntries({ label, yamlKey, description, entries, onChange, text, onText, error }) {
|
|
432
|
+
const t = useGraphEditorStrings();
|
|
433
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
434
|
+
className: "flex flex-col gap-2",
|
|
435
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
436
|
+
label,
|
|
437
|
+
yamlKey,
|
|
438
|
+
description,
|
|
439
|
+
actions: /* @__PURE__ */ jsx(ExpressionToggle, {
|
|
440
|
+
active: text !== void 0,
|
|
441
|
+
onChange: (on) => onText(on ? "" : void 0)
|
|
442
|
+
})
|
|
443
|
+
}), text !== void 0 ? /* @__PURE__ */ jsx(Input, {
|
|
444
|
+
mono: true,
|
|
445
|
+
"aria-label": label,
|
|
446
|
+
value: text,
|
|
447
|
+
error,
|
|
448
|
+
onChange: (e) => onText(e.target.value)
|
|
449
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
450
|
+
withPositionKeys(entries).map(({ key, item: rows }, i) => /* @__PURE__ */ jsxs("div", {
|
|
451
|
+
className: "flex items-start gap-2",
|
|
452
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
453
|
+
className: "flex-1 rounded-md border theme-border p-2",
|
|
454
|
+
children: /* @__PURE__ */ jsx(KeyValueEditor, {
|
|
455
|
+
rows,
|
|
456
|
+
onChange: (next) => onChange(updateAt(entries, i, next)),
|
|
457
|
+
error: void 0
|
|
458
|
+
})
|
|
459
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
460
|
+
icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
|
|
461
|
+
label: t.removeEntry,
|
|
462
|
+
variant: "ghost",
|
|
463
|
+
size: "sm",
|
|
464
|
+
onClick: () => onChange(removeAt(entries, i))
|
|
465
|
+
})]
|
|
466
|
+
}, key)),
|
|
467
|
+
/* @__PURE__ */ jsx(FieldError, { message: error }),
|
|
468
|
+
/* @__PURE__ */ jsx(AddRowButton, {
|
|
469
|
+
label: t.addEntry,
|
|
470
|
+
onClick: () => onChange([...entries, [{
|
|
471
|
+
key: "",
|
|
472
|
+
value: ""
|
|
473
|
+
}]])
|
|
474
|
+
})
|
|
475
|
+
] })]
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** One matrix variable: values listed here, or the selections of a multi-select input. */
|
|
479
|
+
function MatrixVariable({ row, source, onChange, onRemove }) {
|
|
480
|
+
const t = useGraphEditorStrings();
|
|
481
|
+
const ref = readRef(row.values.trim());
|
|
482
|
+
const [fromInput, setFromInput] = useState(ref !== null);
|
|
483
|
+
const { create } = source;
|
|
484
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
485
|
+
className: "flex items-start gap-2",
|
|
486
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
487
|
+
className: "flex flex-1 flex-col gap-2 rounded-md border theme-border p-2",
|
|
488
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
489
|
+
className: "flex items-center gap-2",
|
|
490
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
491
|
+
className: "w-1/3",
|
|
492
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
493
|
+
mono: true,
|
|
494
|
+
"aria-label": t.matrixVariable,
|
|
495
|
+
placeholder: t.matrixVariable,
|
|
496
|
+
value: row.name,
|
|
497
|
+
onChange: (e) => onChange({
|
|
498
|
+
...row,
|
|
499
|
+
name: e.target.value
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
503
|
+
className: "flex-1",
|
|
504
|
+
children: /* @__PURE__ */ jsx(ChoiceButtons, {
|
|
505
|
+
options: [{
|
|
506
|
+
value: "values",
|
|
507
|
+
label: t.matrixValuesListed
|
|
508
|
+
}, {
|
|
509
|
+
value: "input",
|
|
510
|
+
label: t.modeFromInput
|
|
511
|
+
}],
|
|
512
|
+
value: fromInput ? "input" : "values",
|
|
513
|
+
onChange: (mode) => {
|
|
514
|
+
setFromInput(mode === "input");
|
|
515
|
+
onChange({
|
|
516
|
+
...row,
|
|
517
|
+
values: "",
|
|
518
|
+
nested: false
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
})]
|
|
523
|
+
}), fromInput ? /* @__PURE__ */ jsx(InputRefSelect, {
|
|
524
|
+
refs: source.refs,
|
|
525
|
+
types: ["multi-dropdown", "checkbox-group"],
|
|
526
|
+
value: ref,
|
|
527
|
+
label: t.matrixValues,
|
|
528
|
+
onChange: (path) => onChange({
|
|
529
|
+
...row,
|
|
530
|
+
values: refExpression(path)
|
|
531
|
+
}),
|
|
532
|
+
...create ? {
|
|
533
|
+
onNew: () => create("multi-dropdown", (path) => onChange({
|
|
534
|
+
...row,
|
|
535
|
+
values: refExpression(path)
|
|
536
|
+
})),
|
|
537
|
+
newLabel: t.newInput
|
|
538
|
+
} : {}
|
|
539
|
+
}) : /* @__PURE__ */ jsx(Input, {
|
|
540
|
+
mono: true,
|
|
541
|
+
"aria-label": t.matrixValues,
|
|
542
|
+
placeholder: t.matrixValues,
|
|
543
|
+
value: row.values,
|
|
544
|
+
title: row.nested ? t.jsonValue : void 0,
|
|
545
|
+
onChange: (e) => onChange({
|
|
546
|
+
...row,
|
|
547
|
+
values: e.target.value
|
|
548
|
+
})
|
|
549
|
+
})]
|
|
550
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
551
|
+
icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
|
|
552
|
+
label: t.removeVariable,
|
|
553
|
+
variant: "ghost",
|
|
554
|
+
size: "sm",
|
|
555
|
+
onClick: onRemove
|
|
556
|
+
})]
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
/** Every field of a job, saved as one undoable edit. */
|
|
560
|
+
function JobForm({ job, jobs, inputs, workflow, onEdit, onClose, renderShell, onDraft, pending = false }) {
|
|
561
|
+
const t = useGraphEditorStrings();
|
|
562
|
+
const editing = useWorkflowEditing();
|
|
563
|
+
const { needTarget } = editing;
|
|
564
|
+
const newInputs = useNewInputs(inputs);
|
|
565
|
+
const source = {
|
|
566
|
+
...newInputs.source,
|
|
567
|
+
extras: expressionRefs(editing, workflow ?? { jobs }, job)
|
|
568
|
+
};
|
|
569
|
+
const [original] = useState(() => asRecord(jobs[job]));
|
|
570
|
+
const rawNeeds = original["needs"];
|
|
571
|
+
const needsExpression = typeof rawNeeds === "string" ? rawNeeds : void 0;
|
|
572
|
+
const strategy = asRecord(original["strategy"]);
|
|
573
|
+
const rawMatrix = strategy["matrix"];
|
|
574
|
+
const matrixRecord = asRecord(rawMatrix);
|
|
575
|
+
const [name, setName] = useState(job);
|
|
576
|
+
const [needs, setNeeds] = useState(() => Array.isArray(rawNeeds) ? rawNeeds.filter((n) => typeof n === "string") : []);
|
|
577
|
+
const [needsText, setNeedsText] = useState(needsExpression ?? "");
|
|
578
|
+
const [needsTyped, setNeedsTyped] = useState(needsExpression !== void 0);
|
|
579
|
+
const [condition, setCondition] = useState(conditionText(original["if"]));
|
|
580
|
+
const [ssh, setSsh] = useState(() => sshFrom(original["ssh"]));
|
|
581
|
+
const rawRunsOn = original["runs-on"];
|
|
582
|
+
const [runsOn, setRunsOn] = useState(() => runsOnFrom(rawRunsOn));
|
|
583
|
+
const [runsOnTyped, setRunsOnTyped] = useState(typeof rawRunsOn === "string");
|
|
584
|
+
const [runsOnText, setRunsOnText] = useState(typeof rawRunsOn === "string" ? rawRunsOn : "");
|
|
585
|
+
const [workingDirectory, setWorkingDirectory] = useState(text(original["working-directory"]));
|
|
586
|
+
const [timeout, setTimeoutText] = useState(text(original["timeout"]));
|
|
587
|
+
const [env, setEnv] = useState(() => rowsFrom(original["env"], false));
|
|
588
|
+
const [outputs, setOutputs] = useState(() => outputRowsFrom(original["outputs"], job));
|
|
589
|
+
const stepIds = (Array.isArray(original["steps"]) ? original["steps"] : []).flatMap((step) => {
|
|
590
|
+
const id = asRecord(step)["id"];
|
|
591
|
+
return typeof id === "string" && id ? [id] : [];
|
|
592
|
+
});
|
|
593
|
+
const [matrixOn, setMatrixOn] = useState(rawMatrix !== void 0);
|
|
594
|
+
const [matrixMode, setMatrixMode] = useState(typeof rawMatrix === "string" ? "expression" : "variables");
|
|
595
|
+
const [matrixText, setMatrixText] = useState(typeof rawMatrix === "string" ? rawMatrix : "");
|
|
596
|
+
const [matrixRows, setMatrixRows] = useState(() => matrixRowsFrom(rawMatrix));
|
|
597
|
+
const [include, setInclude] = useState(() => entriesFrom(matrixRecord["include"]));
|
|
598
|
+
const [exclude, setExclude] = useState(() => entriesFrom(matrixRecord["exclude"]));
|
|
599
|
+
const [includeText, setIncludeText] = useState(() => typeof matrixRecord["include"] === "string" ? matrixRecord["include"] : void 0);
|
|
600
|
+
const [excludeText, setExcludeText] = useState(() => typeof matrixRecord["exclude"] === "string" ? matrixRecord["exclude"] : void 0);
|
|
601
|
+
const [failFast, setFailFast] = useState(() => flagOf(strategy["fail-fast"]));
|
|
602
|
+
const [maxParallel, setMaxParallel] = useState(text(strategy["max-parallel"]));
|
|
603
|
+
const needsId = useId();
|
|
604
|
+
const needsLint = useFieldProblems("needs", needsId).map((problem) => problem.message).join(" ");
|
|
605
|
+
const others = Object.keys(jobs).filter((other) => other !== job);
|
|
606
|
+
const missing = needs.map(needTarget).filter((target) => !Object.hasOwn(jobs, target));
|
|
607
|
+
const allNeeds = editing.jobNeeds(jobs);
|
|
608
|
+
const isMatrixJob = (other) => asRecord(asRecord(jobs[other])["strategy"])["matrix"] !== void 0;
|
|
609
|
+
const toggleNeed = (other, on) => setNeeds((current) => on ? [...current, other] : current.filter((need) => needTarget(need) !== other));
|
|
610
|
+
const toggleAny = (other, any) => setNeeds((current) => current.map((need) => needTarget(need) === other ? any ? `${other}:any` : other : need));
|
|
611
|
+
const trimmedName = name.trim();
|
|
612
|
+
const matrixNames = matrixRows.map((row) => row.name.trim());
|
|
613
|
+
const matrixError = () => {
|
|
614
|
+
if (!matrixOn) return;
|
|
615
|
+
if (matrixMode === "expression") return matrixText.trim() ? expressionError(matrixText, t) : t.invalidExpressionValue;
|
|
616
|
+
if (matrixRows.length === 0 && include.length === 0 && includeText === void 0) return t.matrixNeedsVariable;
|
|
617
|
+
return matrixRows.some((row, i) => matrixRowInvalid(row, matrixNames, i)) ? t.matrixInvalidVariable : void 0;
|
|
618
|
+
};
|
|
619
|
+
const variablesMode = matrixOn && matrixMode === "variables";
|
|
620
|
+
const errors = {
|
|
621
|
+
name: !editing.isValidJobName(trimmedName) ? t.invalidJobName : trimmedName !== job && Object.hasOwn(jobs, trimmedName) ? t.jobExists : void 0,
|
|
622
|
+
needs: needsTyped ? expressionError(needsText, t) : void 0,
|
|
623
|
+
ssh: sshError(ssh, t),
|
|
624
|
+
runsOn: runsOnTyped ? !runsOnText.trim() ? void 0 : sshTo(ssh, original["ssh"]) ? t.runsOnWithSsh : expressionError(runsOnText, t) : runsOnError(runsOn, !!sshTo(ssh, original["ssh"]), t),
|
|
625
|
+
timeout: durationError(timeout, t),
|
|
626
|
+
env: rowsError(env, ENV_KEY, t.invalidEnvKey, t),
|
|
627
|
+
outputs: outputsError(outputs, t),
|
|
628
|
+
matrix: matrixError(),
|
|
629
|
+
include: !variablesMode ? void 0 : includeText !== void 0 ? expressionError(includeText, t) : entriesError(include, t),
|
|
630
|
+
exclude: !variablesMode ? void 0 : excludeText !== void 0 ? expressionError(excludeText, t) : entriesError(exclude, t),
|
|
631
|
+
failFast: matrixOn ? flagError(failFast, t) : void 0,
|
|
632
|
+
maxParallel: matrixOn ? countError(maxParallel, 1, t.invalidMaxParallel) : void 0
|
|
633
|
+
};
|
|
634
|
+
const nextStrategy = () => {
|
|
635
|
+
if (!matrixOn) return;
|
|
636
|
+
const includeList = includeText !== void 0 ? orUndefined(includeText) : entriesTo(include);
|
|
637
|
+
const excludeList = excludeText !== void 0 ? orUndefined(excludeText) : entriesTo(exclude);
|
|
638
|
+
const next = { matrix: matrixMode === "expression" ? matrixText.trim() : {
|
|
639
|
+
...Object.fromEntries(matrixRows.map((row) => [row.name.trim(), matrixValues(row)])),
|
|
640
|
+
...includeList ? { include: includeList } : {},
|
|
641
|
+
...excludeList ? { exclude: excludeList } : {}
|
|
642
|
+
} };
|
|
643
|
+
if (failFast !== void 0) next["fail-fast"] = failFast;
|
|
644
|
+
const parallel = parseCount(maxParallel);
|
|
645
|
+
if (parallel !== void 0) next["max-parallel"] = parallel;
|
|
646
|
+
return next;
|
|
647
|
+
};
|
|
648
|
+
const patch = diffPatch(original, {
|
|
649
|
+
needs: needsTyped ? orUndefined(needsText) : needs.length > 0 ? needs : void 0,
|
|
650
|
+
if: parseCondition(condition),
|
|
651
|
+
ssh: sshTo(ssh, original["ssh"]),
|
|
652
|
+
"runs-on": runsOnTyped ? orUndefined(runsOnText) : runsOnTo(runsOn),
|
|
653
|
+
"working-directory": orUndefined(workingDirectory),
|
|
654
|
+
timeout: orUndefined(timeout),
|
|
655
|
+
env: rowsTo(env, (value) => value),
|
|
656
|
+
outputs: outputsTo(outputs, trimmedName),
|
|
657
|
+
strategy: nextStrategy()
|
|
658
|
+
});
|
|
659
|
+
const renamed = trimmedName !== job;
|
|
660
|
+
const dirty = renamed || !isEmptyPatch(patch);
|
|
661
|
+
const invalid = Object.values(errors).some(Boolean);
|
|
662
|
+
const addVariableRow = () => setMatrixRows((rows) => rows.length === 0 ? [matrixRow({
|
|
663
|
+
name: "value",
|
|
664
|
+
values: "1, 2"
|
|
665
|
+
})] : rows);
|
|
666
|
+
const saveEdit = newInputs.save(dirty ? {
|
|
667
|
+
type: "updateJob",
|
|
668
|
+
job,
|
|
669
|
+
...renamed ? { name: trimmedName } : {},
|
|
670
|
+
...patch
|
|
671
|
+
} : null);
|
|
672
|
+
onDraft?.(saveEdit, invalid);
|
|
673
|
+
const children = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
674
|
+
newInputs.dialog,
|
|
675
|
+
/* @__PURE__ */ jsx(Input, {
|
|
676
|
+
autoFocus: true,
|
|
677
|
+
mono: true,
|
|
678
|
+
label: t.jobName,
|
|
679
|
+
description: t.help.jobName,
|
|
680
|
+
value: name,
|
|
681
|
+
error: errors.name,
|
|
682
|
+
onChange: (e) => setName(e.target.value)
|
|
683
|
+
}),
|
|
684
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
685
|
+
title: t.sectionDependencies,
|
|
686
|
+
open: true,
|
|
687
|
+
children: [
|
|
688
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
689
|
+
label: t.fields.needs,
|
|
690
|
+
yamlKey: "needs",
|
|
691
|
+
description: needsTyped ? t.help.needsExpression : t.help.needs,
|
|
692
|
+
actions: /* @__PURE__ */ jsx(ExpressionToggle, {
|
|
693
|
+
active: needsTyped,
|
|
694
|
+
onChange: setNeedsTyped
|
|
695
|
+
})
|
|
696
|
+
}),
|
|
697
|
+
needsTyped ? /* @__PURE__ */ jsx(Input, {
|
|
698
|
+
id: needsId,
|
|
699
|
+
mono: true,
|
|
700
|
+
"aria-label": t.fields.needs,
|
|
701
|
+
value: needsText,
|
|
702
|
+
error: errors.needs ?? (needsLint || void 0),
|
|
703
|
+
onChange: (e) => setNeedsText(e.target.value)
|
|
704
|
+
}) : others.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
705
|
+
className: "text-xs theme-muted-text",
|
|
706
|
+
children: t.noOtherJobs
|
|
707
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
708
|
+
id: needsId,
|
|
709
|
+
tabIndex: -1,
|
|
710
|
+
className: "flex flex-col gap-1.5",
|
|
711
|
+
children: [[...others, ...missing].map((other) => {
|
|
712
|
+
const entry = needs.find((need) => needTarget(need) === other);
|
|
713
|
+
const cycle = !entry && editing.dependsOn(allNeeds, other, job);
|
|
714
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
715
|
+
className: "flex items-center gap-2 text-sm",
|
|
716
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
717
|
+
className: cx("flex flex-1 items-center gap-2", cycle ? "cursor-not-allowed opacity-50" : "cursor-pointer"),
|
|
718
|
+
title: cycle ? t.dependsOnThisJob : void 0,
|
|
719
|
+
children: [
|
|
720
|
+
/* @__PURE__ */ jsx("input", {
|
|
721
|
+
type: "checkbox",
|
|
722
|
+
checked: !!entry,
|
|
723
|
+
disabled: cycle,
|
|
724
|
+
onChange: (e) => toggleNeed(other, e.target.checked)
|
|
725
|
+
}),
|
|
726
|
+
/* @__PURE__ */ jsx("span", {
|
|
727
|
+
className: "font-mono",
|
|
728
|
+
children: other
|
|
729
|
+
}),
|
|
730
|
+
!Object.hasOwn(jobs, other) && /* @__PURE__ */ jsx("span", {
|
|
731
|
+
className: "text-xs text-(--theme-error)",
|
|
732
|
+
children: t.missingJob
|
|
733
|
+
})
|
|
734
|
+
]
|
|
735
|
+
}), entry && isMatrixJob(other) && /* @__PURE__ */ jsxs("label", {
|
|
736
|
+
className: "flex cursor-pointer items-center gap-1.5 text-xs theme-muted-text",
|
|
737
|
+
title: t.help.waitForAny,
|
|
738
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
739
|
+
type: "checkbox",
|
|
740
|
+
checked: entry.endsWith(":any"),
|
|
741
|
+
onChange: (e) => toggleAny(other, e.target.checked)
|
|
742
|
+
}), t.waitForAny]
|
|
743
|
+
})]
|
|
744
|
+
}, other);
|
|
745
|
+
}), /* @__PURE__ */ jsx(FieldError, { message: needsLint || void 0 })]
|
|
746
|
+
}),
|
|
747
|
+
/* @__PURE__ */ jsx(ConditionField, {
|
|
748
|
+
scope: "job",
|
|
749
|
+
label: t.fields.condition,
|
|
750
|
+
yamlKey: "if",
|
|
751
|
+
description: t.help.jobIf,
|
|
752
|
+
value: condition,
|
|
753
|
+
onChange: setCondition,
|
|
754
|
+
source
|
|
755
|
+
})
|
|
756
|
+
]
|
|
757
|
+
}),
|
|
758
|
+
/* @__PURE__ */ jsx(Section, {
|
|
759
|
+
title: t.sectionRemoteHost,
|
|
760
|
+
description: t.runsInWorkspace,
|
|
761
|
+
open: !!sshTo(ssh, original["ssh"]),
|
|
762
|
+
alert: !!errors.ssh,
|
|
763
|
+
children: /* @__PURE__ */ jsx(SshEditor, {
|
|
764
|
+
fields: ssh,
|
|
765
|
+
onChange: setSsh,
|
|
766
|
+
error: errors.ssh,
|
|
767
|
+
source
|
|
768
|
+
})
|
|
769
|
+
}),
|
|
770
|
+
/* @__PURE__ */ jsx(Section, {
|
|
771
|
+
title: t.sectionComputeEnvironment,
|
|
772
|
+
description: t.runsOnHint,
|
|
773
|
+
open: runsOnTyped ? !!runsOnText.trim() : !!runsOnTo(runsOn),
|
|
774
|
+
alert: !!errors.runsOn,
|
|
775
|
+
children: /* @__PURE__ */ jsxs(LintScope, {
|
|
776
|
+
at: "runs-on",
|
|
777
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
778
|
+
label: t.fields.runsOn,
|
|
779
|
+
yamlKey: "runs-on",
|
|
780
|
+
description: t.help.runsOn,
|
|
781
|
+
actions: /* @__PURE__ */ jsx(ExpressionToggle, {
|
|
782
|
+
active: runsOnTyped,
|
|
783
|
+
onChange: setRunsOnTyped
|
|
784
|
+
})
|
|
785
|
+
}), runsOnTyped ? /* @__PURE__ */ jsx(Input, {
|
|
786
|
+
mono: true,
|
|
787
|
+
"aria-label": t.fields.runsOn,
|
|
788
|
+
value: runsOnText,
|
|
789
|
+
error: errors.runsOn,
|
|
790
|
+
onChange: (e) => setRunsOnText(e.target.value)
|
|
791
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
792
|
+
/* @__PURE__ */ jsx(ChoiceButtons, {
|
|
793
|
+
options: [{
|
|
794
|
+
value: "name",
|
|
795
|
+
label: t.runsOnByName
|
|
796
|
+
}, {
|
|
797
|
+
value: "target",
|
|
798
|
+
label: t.runsOnByTarget
|
|
799
|
+
}],
|
|
800
|
+
value: runsOn.kind,
|
|
801
|
+
onChange: (kind) => setRunsOn({
|
|
802
|
+
...runsOn,
|
|
803
|
+
kind
|
|
804
|
+
})
|
|
805
|
+
}),
|
|
806
|
+
runsOn.kind === "name" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
807
|
+
/* @__PURE__ */ jsx(Input, {
|
|
808
|
+
mono: true,
|
|
809
|
+
...labelled(t.fields.environmentCluster, "cluster"),
|
|
810
|
+
description: t.help.environmentCluster,
|
|
811
|
+
value: runsOn.cluster,
|
|
812
|
+
onChange: (e) => setRunsOn({
|
|
813
|
+
...runsOn,
|
|
814
|
+
cluster: e.target.value
|
|
815
|
+
})
|
|
816
|
+
}),
|
|
817
|
+
/* @__PURE__ */ jsx(Input, {
|
|
818
|
+
mono: true,
|
|
819
|
+
...labelled(t.fields.environmentName, "name"),
|
|
820
|
+
description: t.help.environmentName,
|
|
821
|
+
value: runsOn.name,
|
|
822
|
+
onChange: (e) => setRunsOn({
|
|
823
|
+
...runsOn,
|
|
824
|
+
name: e.target.value
|
|
825
|
+
})
|
|
826
|
+
}),
|
|
827
|
+
/* @__PURE__ */ jsx(LabelledField, {
|
|
828
|
+
label: t.fields.schedulingParams,
|
|
829
|
+
yamlKey: "params",
|
|
830
|
+
description: t.help.schedulingParams,
|
|
831
|
+
children: /* @__PURE__ */ jsx(KeyValueEditor, {
|
|
832
|
+
rows: runsOn.params,
|
|
833
|
+
onChange: (params) => setRunsOn({
|
|
834
|
+
...runsOn,
|
|
835
|
+
params
|
|
836
|
+
}),
|
|
837
|
+
error: void 0
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
841
|
+
/* @__PURE__ */ jsx(LabelledField, {
|
|
842
|
+
label: t.fields.targetMode,
|
|
843
|
+
yamlKey: "mode",
|
|
844
|
+
description: t.help.targetMode,
|
|
845
|
+
children: /* @__PURE__ */ jsx(ChoiceButtons, {
|
|
846
|
+
options: TARGET_MODES.map((mode) => ({
|
|
847
|
+
value: mode,
|
|
848
|
+
label: t.targetModes[mode]
|
|
849
|
+
})),
|
|
850
|
+
value: runsOn.mode,
|
|
851
|
+
onChange: (mode) => setRunsOn({
|
|
852
|
+
...runsOn,
|
|
853
|
+
mode
|
|
854
|
+
})
|
|
855
|
+
})
|
|
856
|
+
}),
|
|
857
|
+
/* @__PURE__ */ jsx(Input, {
|
|
858
|
+
mono: true,
|
|
859
|
+
...labelled(t.fields.targetId, "targetId"),
|
|
860
|
+
description: t.help.targetId,
|
|
861
|
+
value: runsOn.targetId,
|
|
862
|
+
onChange: (e) => setRunsOn({
|
|
863
|
+
...runsOn,
|
|
864
|
+
targetId: e.target.value
|
|
865
|
+
})
|
|
866
|
+
}),
|
|
867
|
+
/* @__PURE__ */ jsx(Input, {
|
|
868
|
+
mono: true,
|
|
869
|
+
...labelled(t.fields.environmentId, "environmentId"),
|
|
870
|
+
description: t.help.environmentId,
|
|
871
|
+
value: runsOn.environmentId,
|
|
872
|
+
onChange: (e) => setRunsOn({
|
|
873
|
+
...runsOn,
|
|
874
|
+
environmentId: e.target.value
|
|
875
|
+
})
|
|
876
|
+
}),
|
|
877
|
+
/* @__PURE__ */ jsx(LabelledField, {
|
|
878
|
+
label: t.fields.schedulingParams,
|
|
879
|
+
yamlKey: "schedulingParams",
|
|
880
|
+
description: t.help.schedulingParams,
|
|
881
|
+
children: /* @__PURE__ */ jsx(KeyValueEditor, {
|
|
882
|
+
rows: runsOn.schedulingParams,
|
|
883
|
+
onChange: (schedulingParams) => setRunsOn({
|
|
884
|
+
...runsOn,
|
|
885
|
+
schedulingParams
|
|
886
|
+
}),
|
|
887
|
+
error: void 0
|
|
888
|
+
})
|
|
889
|
+
}),
|
|
890
|
+
/* @__PURE__ */ jsx(ToggleField, {
|
|
891
|
+
label: t.fields.newWorker,
|
|
892
|
+
yamlKey: "newWorker",
|
|
893
|
+
description: t.help.newWorker,
|
|
894
|
+
checked: runsOn.newWorker === true,
|
|
895
|
+
onChange: (newWorker) => setRunsOn({
|
|
896
|
+
...runsOn,
|
|
897
|
+
newWorker
|
|
898
|
+
})
|
|
899
|
+
}),
|
|
900
|
+
/* @__PURE__ */ jsx(Input, {
|
|
901
|
+
mono: true,
|
|
902
|
+
...labelled(t.fields.workerId, "workerId"),
|
|
903
|
+
description: t.help.workerId,
|
|
904
|
+
value: runsOn.workerId,
|
|
905
|
+
onChange: (e) => setRunsOn({
|
|
906
|
+
...runsOn,
|
|
907
|
+
workerId: e.target.value
|
|
908
|
+
})
|
|
909
|
+
})
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsx(FieldError, { message: errors.runsOn })
|
|
912
|
+
] })]
|
|
913
|
+
})
|
|
914
|
+
}),
|
|
915
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
916
|
+
title: t.sectionMatrix,
|
|
917
|
+
open: matrixOn,
|
|
918
|
+
alert: !!(errors.matrix || errors.include || errors.exclude || errors.failFast || errors.maxParallel),
|
|
919
|
+
children: [/* @__PURE__ */ jsx(ToggleField, {
|
|
920
|
+
label: t.fields.matrix,
|
|
921
|
+
yamlKey: "strategy.matrix",
|
|
922
|
+
description: t.matrixOn,
|
|
923
|
+
checked: matrixOn,
|
|
924
|
+
onChange: (on) => {
|
|
925
|
+
setMatrixOn(on);
|
|
926
|
+
if (on && matrixMode === "variables") addVariableRow();
|
|
927
|
+
}
|
|
928
|
+
}), matrixOn && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
929
|
+
typeof rawMatrix === "string" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
930
|
+
label: t.fields.matrixSource,
|
|
931
|
+
description: t.help.matrixMode
|
|
932
|
+
}), /* @__PURE__ */ jsx(ChoiceButtons, {
|
|
933
|
+
options: [{
|
|
934
|
+
value: "variables",
|
|
935
|
+
label: t.matrixFromVariables
|
|
936
|
+
}, {
|
|
937
|
+
value: "expression",
|
|
938
|
+
label: t.matrixFromExpression
|
|
939
|
+
}],
|
|
940
|
+
value: matrixMode,
|
|
941
|
+
onChange: (mode) => {
|
|
942
|
+
setMatrixMode(mode);
|
|
943
|
+
if (mode === "variables") addVariableRow();
|
|
944
|
+
}
|
|
945
|
+
})] }),
|
|
946
|
+
matrixMode === "expression" ? /* @__PURE__ */ jsx(Input, {
|
|
947
|
+
mono: true,
|
|
948
|
+
...labelled(t.fields.matrixExpression, "strategy.matrix"),
|
|
949
|
+
description: t.help.matrixExpression,
|
|
950
|
+
value: matrixText,
|
|
951
|
+
error: errors.matrix,
|
|
952
|
+
onChange: (e) => setMatrixText(e.target.value)
|
|
953
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
954
|
+
/* @__PURE__ */ jsx("div", {
|
|
955
|
+
className: "text-xs theme-muted-text",
|
|
956
|
+
children: t.help.matrixVariables
|
|
957
|
+
}),
|
|
958
|
+
matrixRows.map((row, i) => /* @__PURE__ */ jsx(MatrixVariable, {
|
|
959
|
+
row,
|
|
960
|
+
source,
|
|
961
|
+
onChange: (next) => setMatrixRows((rows) => updateAt(rows, i, next)),
|
|
962
|
+
onRemove: () => setMatrixRows((rows) => removeAt(rows, i))
|
|
963
|
+
}, row.key)),
|
|
964
|
+
/* @__PURE__ */ jsx(FieldError, { message: errors.matrix }),
|
|
965
|
+
/* @__PURE__ */ jsx(AddRowButton, {
|
|
966
|
+
label: t.addVariable,
|
|
967
|
+
onClick: () => setMatrixRows((rows) => [...rows, matrixRow({
|
|
968
|
+
name: "",
|
|
969
|
+
values: ""
|
|
970
|
+
})])
|
|
971
|
+
}),
|
|
972
|
+
/* @__PURE__ */ jsx(MatrixEntries, {
|
|
973
|
+
label: t.fields.include,
|
|
974
|
+
yamlKey: "include",
|
|
975
|
+
description: t.help.include,
|
|
976
|
+
entries: include,
|
|
977
|
+
onChange: setInclude,
|
|
978
|
+
text: includeText,
|
|
979
|
+
onText: setIncludeText,
|
|
980
|
+
error: errors.include
|
|
981
|
+
}),
|
|
982
|
+
/* @__PURE__ */ jsx(MatrixEntries, {
|
|
983
|
+
label: t.fields.exclude,
|
|
984
|
+
yamlKey: "exclude",
|
|
985
|
+
description: t.help.exclude,
|
|
986
|
+
entries: exclude,
|
|
987
|
+
onChange: setExclude,
|
|
988
|
+
text: excludeText,
|
|
989
|
+
onText: setExcludeText,
|
|
990
|
+
error: errors.exclude
|
|
991
|
+
})
|
|
992
|
+
] }),
|
|
993
|
+
/* @__PURE__ */ jsx(FlagField, {
|
|
994
|
+
label: t.fields.failFast,
|
|
995
|
+
yamlKey: "fail-fast",
|
|
996
|
+
description: t.help.failFast,
|
|
997
|
+
value: failFast,
|
|
998
|
+
original: strategy["fail-fast"],
|
|
999
|
+
fallback: true,
|
|
1000
|
+
onChange: setFailFast,
|
|
1001
|
+
source
|
|
1002
|
+
}),
|
|
1003
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1004
|
+
mono: true,
|
|
1005
|
+
...labelled(t.fields.maxParallel, "max-parallel"),
|
|
1006
|
+
description: t.help.maxParallel,
|
|
1007
|
+
value: maxParallel,
|
|
1008
|
+
error: errors.maxParallel,
|
|
1009
|
+
onChange: (e) => setMaxParallel(e.target.value)
|
|
1010
|
+
})
|
|
1011
|
+
] })]
|
|
1012
|
+
}),
|
|
1013
|
+
/* @__PURE__ */ jsx(EnvSection, {
|
|
1014
|
+
description: t.help.jobEnv,
|
|
1015
|
+
rows: env,
|
|
1016
|
+
onChange: setEnv,
|
|
1017
|
+
error: errors.env,
|
|
1018
|
+
source
|
|
1019
|
+
}),
|
|
1020
|
+
/* @__PURE__ */ jsx(Section, {
|
|
1021
|
+
title: t.sectionOutputs,
|
|
1022
|
+
description: t.help.outputs,
|
|
1023
|
+
open: outputs.length > 0,
|
|
1024
|
+
alert: !!errors.outputs,
|
|
1025
|
+
children: /* @__PURE__ */ jsx(OutputsEditor, {
|
|
1026
|
+
rows: outputs,
|
|
1027
|
+
onChange: setOutputs,
|
|
1028
|
+
stepIds,
|
|
1029
|
+
error: errors.outputs
|
|
1030
|
+
})
|
|
1031
|
+
}),
|
|
1032
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
1033
|
+
title: t.sectionAdvanced,
|
|
1034
|
+
open: !!workingDirectory || !!timeout,
|
|
1035
|
+
alert: !!errors.timeout,
|
|
1036
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
1037
|
+
mono: true,
|
|
1038
|
+
...labelled(t.fields.workingDirectory, "working-directory"),
|
|
1039
|
+
description: t.help.jobWorkingDirectory,
|
|
1040
|
+
value: workingDirectory,
|
|
1041
|
+
onChange: (e) => setWorkingDirectory(e.target.value)
|
|
1042
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1043
|
+
mono: true,
|
|
1044
|
+
...labelled(t.fields.timeout, "timeout"),
|
|
1045
|
+
description: t.help.jobTimeout,
|
|
1046
|
+
value: timeout,
|
|
1047
|
+
error: errors.timeout,
|
|
1048
|
+
onChange: (e) => setTimeoutText(e.target.value)
|
|
1049
|
+
})]
|
|
1050
|
+
})
|
|
1051
|
+
] });
|
|
1052
|
+
const shellProps = {
|
|
1053
|
+
title: rawMatrix !== void 0 ? t.editMatrix : t.editJob,
|
|
1054
|
+
onClose,
|
|
1055
|
+
dirty: dirty || pending,
|
|
1056
|
+
saveDisabled: invalid,
|
|
1057
|
+
locked: newInputs.dialog !== null,
|
|
1058
|
+
onSubmit: () => {
|
|
1059
|
+
if (saveEdit) onEdit(saveEdit);
|
|
1060
|
+
onClose();
|
|
1061
|
+
},
|
|
1062
|
+
children
|
|
1063
|
+
};
|
|
1064
|
+
return renderShell ? renderShell(shellProps) : /* @__PURE__ */ jsx(DialogShell, { ...shellProps });
|
|
1065
|
+
}
|
|
1066
|
+
/** Every field of a step, saved as one undoable edit. */
|
|
1067
|
+
function StepForm({ job, index, steps, inputs, workflow, usesSuggestions, onEdit, onClose, renderShell, onDraft, pending = false, adding = false }) {
|
|
1068
|
+
const t = useGraphEditorStrings();
|
|
1069
|
+
const editing = useWorkflowEditing();
|
|
1070
|
+
const actions = useWorkflowActions();
|
|
1071
|
+
const newInputs = useNewInputs(inputs);
|
|
1072
|
+
const source = {
|
|
1073
|
+
...newInputs.source,
|
|
1074
|
+
extras: expressionRefs(editing, workflow, job)
|
|
1075
|
+
};
|
|
1076
|
+
const [original] = useState(() => asRecord(steps[index]));
|
|
1077
|
+
const rawRetry = original["retry"];
|
|
1078
|
+
const retry = asRecord(rawRetry);
|
|
1079
|
+
const [name, setName] = useState(text(original["name"]));
|
|
1080
|
+
const [kind, setKind] = useState(original["uses"] !== void 0 ? "uses" : "run");
|
|
1081
|
+
const [run, setRun] = useState(text(original["run"]));
|
|
1082
|
+
const [shell, setShell] = useState(text(original["shell"]));
|
|
1083
|
+
const [uses, setUses] = useState(text(original["uses"]));
|
|
1084
|
+
const [initialWith] = useState(() => withDraftFrom(original["with"], actions));
|
|
1085
|
+
const [withDraft, setWithDraft] = useState(initialWith);
|
|
1086
|
+
const [condition, setCondition] = useState(conditionText(original["if"]));
|
|
1087
|
+
const [workingDirectory, setWorkingDirectory] = useState(text(original["working-directory"]));
|
|
1088
|
+
const [id, setId] = useState(text(original["id"]));
|
|
1089
|
+
const [timeout, setTimeoutText] = useState(text(original["timeout"]));
|
|
1090
|
+
const [ignoreErrors, setIgnoreErrors] = useState(() => flagOf(original["ignore-errors"]));
|
|
1091
|
+
const [earlyCancel, setEarlyCancel] = useState(original["early-cancel"] === "any-job-failed");
|
|
1092
|
+
const [cleanup, setCleanup] = useState(text(original["cleanup"]));
|
|
1093
|
+
const [retryOn, setRetryOn] = useState(rawRetry !== void 0);
|
|
1094
|
+
const [maxRetries, setMaxRetries] = useState(text(retry["max-retries"]));
|
|
1095
|
+
const [interval, setIntervalText] = useState(text(retry["interval"]));
|
|
1096
|
+
const [retryTimeout, setRetryTimeout] = useState(text(retry["timeout"]));
|
|
1097
|
+
const [cleanOnRetry, setCleanOnRetry] = useState(() => flagOf(retry["clean-on-retry"]));
|
|
1098
|
+
const [env, setEnv] = useState(() => rowsFrom(original["env"], false));
|
|
1099
|
+
const [sshMode, setSshMode] = useState(original["ssh"] === null ? "none" : original["ssh"] !== void 0 ? "custom" : "inherit");
|
|
1100
|
+
const [ssh, setSsh] = useState(() => sshFrom(original["ssh"]));
|
|
1101
|
+
const trimmedUses = uses.trim();
|
|
1102
|
+
const errors = {
|
|
1103
|
+
run: kind === "run" && !run.trim() ? t.required : void 0,
|
|
1104
|
+
shell: kind === "run" && !SHELL.test(shell.trim()) ? t.invalidShell : void 0,
|
|
1105
|
+
uses: kind !== "uses" ? void 0 : !trimmedUses ? t.required : usesKind(trimmedUses, actions) === "other" ? t.invalidUses : void 0,
|
|
1106
|
+
with: kind === "uses" ? withError(trimmedUses, actions, withDraft, original["with"], t) : void 0,
|
|
1107
|
+
id: id.trim() && !editing.isValidJobName(id.trim()) ? t.invalidJobName : void 0,
|
|
1108
|
+
timeout: durationError(timeout, t),
|
|
1109
|
+
ignoreErrors: flagError(ignoreErrors, t),
|
|
1110
|
+
maxRetries: retryOn ? countError(maxRetries, 0, t.invalidCount) : void 0,
|
|
1111
|
+
interval: retryOn ? durationError(interval, t) : void 0,
|
|
1112
|
+
retryTimeout: retryOn ? durationError(retryTimeout, t) : void 0,
|
|
1113
|
+
cleanOnRetry: retryOn ? flagError(cleanOnRetry, t) : void 0,
|
|
1114
|
+
env: rowsError(env, ENV_KEY, t.invalidEnvKey, t),
|
|
1115
|
+
ssh: sshMode === "custom" ? sshError(ssh, t) : void 0
|
|
1116
|
+
};
|
|
1117
|
+
const nextRetry = () => {
|
|
1118
|
+
if (!retryOn) return;
|
|
1119
|
+
const next = {};
|
|
1120
|
+
const retries = parseCount(maxRetries);
|
|
1121
|
+
if (retries !== void 0) next["max-retries"] = retries;
|
|
1122
|
+
if (interval.trim()) next["interval"] = interval.trim();
|
|
1123
|
+
if (retryTimeout.trim()) next["timeout"] = retryTimeout.trim();
|
|
1124
|
+
if (cleanOnRetry !== void 0) next["clean-on-retry"] = cleanOnRetry;
|
|
1125
|
+
return next;
|
|
1126
|
+
};
|
|
1127
|
+
const patch = diffPatch(original, {
|
|
1128
|
+
name: orUndefined(name),
|
|
1129
|
+
run: kind === "run" ? run : void 0,
|
|
1130
|
+
shell: kind === "run" ? orUndefined(shell) : void 0,
|
|
1131
|
+
uses: kind === "uses" ? trimmedUses : void 0,
|
|
1132
|
+
with: kind === "uses" ? withValue(trimmedUses, actions, withDraft, initialWith, text(original["uses"]), original["with"]) : void 0,
|
|
1133
|
+
if: parseCondition(condition),
|
|
1134
|
+
"working-directory": original["working-directory"] === null && !workingDirectory.trim() ? null : orUndefined(workingDirectory),
|
|
1135
|
+
id: orUndefined(id),
|
|
1136
|
+
timeout: orUndefined(timeout),
|
|
1137
|
+
"ignore-errors": ignoreErrors,
|
|
1138
|
+
"early-cancel": earlyCancel ? "any-job-failed" : void 0,
|
|
1139
|
+
cleanup: cleanup.trim() ? cleanup : void 0,
|
|
1140
|
+
retry: nextRetry(),
|
|
1141
|
+
env: rowsTo(env, (value) => value),
|
|
1142
|
+
ssh: sshMode === "inherit" ? void 0 : sshMode === "none" ? null : sshTo(ssh, original["ssh"])
|
|
1143
|
+
});
|
|
1144
|
+
const dirty = !isEmptyPatch(patch);
|
|
1145
|
+
const invalid = Object.values(errors).some(Boolean);
|
|
1146
|
+
const edit = (graphEdit) => {
|
|
1147
|
+
onEdit(graphEdit);
|
|
1148
|
+
onClose();
|
|
1149
|
+
};
|
|
1150
|
+
const fromId = text(original["id"]).trim();
|
|
1151
|
+
const references = fromId && id.trim() && id.trim() !== fromId ? stepIdEdits(workflow, steps, job, index, fromId, id.trim()) : [];
|
|
1152
|
+
const saveEdit = newInputs.save(dirty ? editing.batchOf([{
|
|
1153
|
+
type: "updateStep",
|
|
1154
|
+
job,
|
|
1155
|
+
index,
|
|
1156
|
+
...patch
|
|
1157
|
+
}, ...references]) : null);
|
|
1158
|
+
onDraft?.(saveEdit, invalid);
|
|
1159
|
+
const children = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1160
|
+
newInputs.dialog,
|
|
1161
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1162
|
+
autoFocus: true,
|
|
1163
|
+
...labelled(t.stepName, "name"),
|
|
1164
|
+
description: t.help.stepName,
|
|
1165
|
+
value: name,
|
|
1166
|
+
onChange: (e) => setName(e.target.value)
|
|
1167
|
+
}),
|
|
1168
|
+
/* @__PURE__ */ jsx("div", {
|
|
1169
|
+
className: "text-xs theme-muted-text",
|
|
1170
|
+
children: t.help.stepKind
|
|
1171
|
+
}),
|
|
1172
|
+
/* @__PURE__ */ jsx(ChoiceButtons, {
|
|
1173
|
+
options: [{
|
|
1174
|
+
value: "run",
|
|
1175
|
+
label: t.stepRun
|
|
1176
|
+
}, {
|
|
1177
|
+
value: "uses",
|
|
1178
|
+
label: t.stepUses
|
|
1179
|
+
}],
|
|
1180
|
+
value: kind,
|
|
1181
|
+
onChange: setKind
|
|
1182
|
+
}),
|
|
1183
|
+
kind === "run" ? /* @__PURE__ */ jsx(ScriptField, {
|
|
1184
|
+
label: t.fields.run,
|
|
1185
|
+
yamlKey: "run",
|
|
1186
|
+
description: t.help.run,
|
|
1187
|
+
rows: 8,
|
|
1188
|
+
value: run,
|
|
1189
|
+
error: errors.run,
|
|
1190
|
+
placeholder: "echo \"Hello World\"",
|
|
1191
|
+
onChange: setRun,
|
|
1192
|
+
source
|
|
1193
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(UsesPicker, {
|
|
1194
|
+
uses: trimmedUses,
|
|
1195
|
+
onUses: setUses,
|
|
1196
|
+
choices: usesSuggestions,
|
|
1197
|
+
draft: withDraft,
|
|
1198
|
+
onDraft: setWithDraft,
|
|
1199
|
+
error: errors.uses
|
|
1200
|
+
}), /* @__PURE__ */ jsx(WithEditor, {
|
|
1201
|
+
uses: trimmedUses,
|
|
1202
|
+
draft: withDraft,
|
|
1203
|
+
original: original["with"],
|
|
1204
|
+
onChange: setWithDraft,
|
|
1205
|
+
error: errors.with,
|
|
1206
|
+
source
|
|
1207
|
+
})] }),
|
|
1208
|
+
/* @__PURE__ */ jsx(Section, {
|
|
1209
|
+
title: t.sectionCleanup,
|
|
1210
|
+
open: !!cleanup.trim(),
|
|
1211
|
+
children: /* @__PURE__ */ jsx(ScriptField, {
|
|
1212
|
+
label: t.fields.cleanup,
|
|
1213
|
+
yamlKey: "cleanup",
|
|
1214
|
+
description: t.help.cleanup,
|
|
1215
|
+
value: cleanup,
|
|
1216
|
+
onChange: setCleanup,
|
|
1217
|
+
source
|
|
1218
|
+
})
|
|
1219
|
+
}),
|
|
1220
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
1221
|
+
title: t.sectionBehavior,
|
|
1222
|
+
open: !!condition || !!timeout || ignoreErrors !== void 0 || earlyCancel,
|
|
1223
|
+
alert: !!(errors.timeout || errors.ignoreErrors),
|
|
1224
|
+
children: [
|
|
1225
|
+
/* @__PURE__ */ jsx(ConditionField, {
|
|
1226
|
+
scope: "step",
|
|
1227
|
+
label: t.fields.condition,
|
|
1228
|
+
yamlKey: "if",
|
|
1229
|
+
description: t.help.stepIf,
|
|
1230
|
+
value: condition,
|
|
1231
|
+
onChange: setCondition,
|
|
1232
|
+
source
|
|
1233
|
+
}),
|
|
1234
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1235
|
+
mono: true,
|
|
1236
|
+
...labelled(t.fields.timeout, "timeout"),
|
|
1237
|
+
description: t.help.stepTimeout,
|
|
1238
|
+
value: timeout,
|
|
1239
|
+
error: errors.timeout,
|
|
1240
|
+
onChange: (e) => setTimeoutText(e.target.value)
|
|
1241
|
+
}),
|
|
1242
|
+
/* @__PURE__ */ jsx(FlagField, {
|
|
1243
|
+
label: t.fields.ignoreErrors,
|
|
1244
|
+
yamlKey: "ignore-errors",
|
|
1245
|
+
description: t.help.ignoreErrors,
|
|
1246
|
+
value: ignoreErrors,
|
|
1247
|
+
original: original["ignore-errors"],
|
|
1248
|
+
onChange: setIgnoreErrors,
|
|
1249
|
+
source
|
|
1250
|
+
}),
|
|
1251
|
+
/* @__PURE__ */ jsx(ToggleField, {
|
|
1252
|
+
label: t.fields.earlyCancel,
|
|
1253
|
+
yamlKey: "early-cancel",
|
|
1254
|
+
description: t.help.earlyCancel,
|
|
1255
|
+
checked: earlyCancel,
|
|
1256
|
+
onChange: setEarlyCancel
|
|
1257
|
+
})
|
|
1258
|
+
]
|
|
1259
|
+
}),
|
|
1260
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
1261
|
+
title: t.sectionRetry,
|
|
1262
|
+
open: retryOn,
|
|
1263
|
+
alert: !!(errors.maxRetries || errors.interval || errors.retryTimeout || errors.cleanOnRetry),
|
|
1264
|
+
children: [/* @__PURE__ */ jsx(ToggleField, {
|
|
1265
|
+
label: t.fields.retry,
|
|
1266
|
+
yamlKey: "retry",
|
|
1267
|
+
description: t.help.retry,
|
|
1268
|
+
checked: retryOn,
|
|
1269
|
+
onChange: setRetryOn
|
|
1270
|
+
}), retryOn && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1271
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1272
|
+
mono: true,
|
|
1273
|
+
...labelled(t.fields.maxRetries, "max-retries"),
|
|
1274
|
+
description: t.help.maxRetries,
|
|
1275
|
+
value: maxRetries,
|
|
1276
|
+
error: errors.maxRetries,
|
|
1277
|
+
placeholder: "10",
|
|
1278
|
+
onChange: (e) => setMaxRetries(e.target.value)
|
|
1279
|
+
}),
|
|
1280
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1281
|
+
mono: true,
|
|
1282
|
+
...labelled(t.fields.retryInterval, "interval"),
|
|
1283
|
+
description: t.help.retryInterval,
|
|
1284
|
+
value: interval,
|
|
1285
|
+
error: errors.interval,
|
|
1286
|
+
placeholder: "5s",
|
|
1287
|
+
onChange: (e) => setIntervalText(e.target.value)
|
|
1288
|
+
}),
|
|
1289
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1290
|
+
mono: true,
|
|
1291
|
+
...labelled(t.fields.retryTimeout, "timeout"),
|
|
1292
|
+
description: t.help.retryTimeout,
|
|
1293
|
+
value: retryTimeout,
|
|
1294
|
+
error: errors.retryTimeout,
|
|
1295
|
+
placeholder: "30s",
|
|
1296
|
+
onChange: (e) => setRetryTimeout(e.target.value)
|
|
1297
|
+
}),
|
|
1298
|
+
/* @__PURE__ */ jsx(FlagField, {
|
|
1299
|
+
label: t.fields.cleanOnRetry,
|
|
1300
|
+
yamlKey: "clean-on-retry",
|
|
1301
|
+
description: t.help.cleanOnRetry,
|
|
1302
|
+
value: cleanOnRetry,
|
|
1303
|
+
original: retry["clean-on-retry"],
|
|
1304
|
+
fallback: true,
|
|
1305
|
+
onChange: setCleanOnRetry,
|
|
1306
|
+
source
|
|
1307
|
+
})
|
|
1308
|
+
] })]
|
|
1309
|
+
}),
|
|
1310
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
1311
|
+
title: t.sectionRemoteHost,
|
|
1312
|
+
description: t.help.stepSsh,
|
|
1313
|
+
open: sshMode !== "inherit",
|
|
1314
|
+
alert: !!errors.ssh,
|
|
1315
|
+
children: [/* @__PURE__ */ jsx(ChoiceButtons, {
|
|
1316
|
+
options: [
|
|
1317
|
+
{
|
|
1318
|
+
value: "inherit",
|
|
1319
|
+
label: t.sshInherit
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
value: "custom",
|
|
1323
|
+
label: t.sshCustom
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
value: "none",
|
|
1327
|
+
label: t.sshNone
|
|
1328
|
+
}
|
|
1329
|
+
],
|
|
1330
|
+
value: sshMode,
|
|
1331
|
+
onChange: setSshMode
|
|
1332
|
+
}), sshMode === "custom" && /* @__PURE__ */ jsx(SshEditor, {
|
|
1333
|
+
fields: ssh,
|
|
1334
|
+
onChange: setSsh,
|
|
1335
|
+
error: errors.ssh,
|
|
1336
|
+
source
|
|
1337
|
+
})]
|
|
1338
|
+
}),
|
|
1339
|
+
/* @__PURE__ */ jsx(EnvSection, {
|
|
1340
|
+
description: t.help.stepEnv,
|
|
1341
|
+
rows: env,
|
|
1342
|
+
onChange: setEnv,
|
|
1343
|
+
error: errors.env,
|
|
1344
|
+
source
|
|
1345
|
+
}),
|
|
1346
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
1347
|
+
title: t.sectionAdvanced,
|
|
1348
|
+
open: !!workingDirectory || !!id || !!shell,
|
|
1349
|
+
alert: !!(errors.id || errors.shell),
|
|
1350
|
+
children: [
|
|
1351
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1352
|
+
mono: true,
|
|
1353
|
+
...labelled(t.fields.workingDirectory, "working-directory"),
|
|
1354
|
+
description: t.help.stepWorkingDirectory,
|
|
1355
|
+
value: workingDirectory,
|
|
1356
|
+
onChange: (e) => setWorkingDirectory(e.target.value)
|
|
1357
|
+
}),
|
|
1358
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1359
|
+
mono: true,
|
|
1360
|
+
...labelled(t.fields.stepId, "id"),
|
|
1361
|
+
description: t.help.stepId,
|
|
1362
|
+
value: id,
|
|
1363
|
+
error: errors.id,
|
|
1364
|
+
onChange: (e) => setId(e.target.value)
|
|
1365
|
+
}),
|
|
1366
|
+
kind === "run" && /* @__PURE__ */ jsx(Input, {
|
|
1367
|
+
mono: true,
|
|
1368
|
+
...labelled(t.fields.shell, "shell"),
|
|
1369
|
+
description: t.help.shell,
|
|
1370
|
+
value: shell,
|
|
1371
|
+
error: errors.shell,
|
|
1372
|
+
placeholder: "bash -e {0}",
|
|
1373
|
+
onChange: (e) => setShell(e.target.value)
|
|
1374
|
+
})
|
|
1375
|
+
]
|
|
1376
|
+
})
|
|
1377
|
+
] });
|
|
1378
|
+
const shellProps = {
|
|
1379
|
+
title: t.editStep,
|
|
1380
|
+
onClose,
|
|
1381
|
+
dirty: dirty || pending,
|
|
1382
|
+
saveDisabled: invalid,
|
|
1383
|
+
locked: newInputs.dialog !== null,
|
|
1384
|
+
onSubmit: () => {
|
|
1385
|
+
if (saveEdit) onEdit(saveEdit);
|
|
1386
|
+
onClose();
|
|
1387
|
+
},
|
|
1388
|
+
footerStart: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1389
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
1390
|
+
icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, { className: "h-4 w-4" }),
|
|
1391
|
+
label: t.deleteStep,
|
|
1392
|
+
variant: "ghost",
|
|
1393
|
+
size: "sm",
|
|
1394
|
+
onClick: () => adding ? onClose() : edit({
|
|
1395
|
+
type: "deleteStep",
|
|
1396
|
+
job,
|
|
1397
|
+
index
|
|
1398
|
+
})
|
|
1399
|
+
}),
|
|
1400
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
1401
|
+
icon: /* @__PURE__ */ jsx(FaArrowUp, { className: "h-4 w-4" }),
|
|
1402
|
+
label: t.moveUp,
|
|
1403
|
+
variant: "ghost",
|
|
1404
|
+
size: "sm",
|
|
1405
|
+
disabled: dirty || pending || index === 0,
|
|
1406
|
+
onClick: () => edit({
|
|
1407
|
+
type: "moveStep",
|
|
1408
|
+
job,
|
|
1409
|
+
from: index,
|
|
1410
|
+
to: index - 1
|
|
1411
|
+
})
|
|
1412
|
+
}),
|
|
1413
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
1414
|
+
icon: /* @__PURE__ */ jsx(FaArrowDown, { className: "h-4 w-4" }),
|
|
1415
|
+
label: t.moveDown,
|
|
1416
|
+
variant: "ghost",
|
|
1417
|
+
size: "sm",
|
|
1418
|
+
disabled: dirty || pending || index >= steps.length - 1,
|
|
1419
|
+
onClick: () => edit({
|
|
1420
|
+
type: "moveStep",
|
|
1421
|
+
job,
|
|
1422
|
+
from: index,
|
|
1423
|
+
to: index + 2
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
] }),
|
|
1427
|
+
children
|
|
1428
|
+
};
|
|
1429
|
+
return renderShell ? renderShell(shellProps) : /* @__PURE__ */ jsx(DialogShell, { ...shellProps });
|
|
1430
|
+
}
|
|
1431
|
+
function openingYaml(view, source, read) {
|
|
1432
|
+
if (view === "form" || source === void 0) return null;
|
|
1433
|
+
try {
|
|
1434
|
+
return read(source);
|
|
1435
|
+
} catch {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function stepIdEdits(workflow, steps, job, index, from, to) {
|
|
1440
|
+
const id = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1441
|
+
const own = new RegExp(`(?<![A-Za-z0-9_.-])steps\\.${id}(?![A-Za-z0-9_-])`, "g");
|
|
1442
|
+
const needed = new RegExp(`(?<![A-Za-z0-9_.-])needs\\.${job}\\.steps\\.${id}(?![A-Za-z0-9_-])`, "g");
|
|
1443
|
+
const rewrite = (value, inJob) => {
|
|
1444
|
+
if (typeof value === "string") return value.replace(/\$\{\{[\s\S]*?\}\}/g, (expression) => {
|
|
1445
|
+
const outside = expression.replace(needed, `needs.${job}.steps.${to}`);
|
|
1446
|
+
return inJob ? outside.replace(own, `steps.${to}`) : outside;
|
|
1447
|
+
});
|
|
1448
|
+
if (Array.isArray(value)) return value.map((item) => rewrite(item, inJob));
|
|
1449
|
+
return value && typeof value === "object" ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item, inJob)])) : value;
|
|
1450
|
+
};
|
|
1451
|
+
const changed = (record, inJob) => {
|
|
1452
|
+
const set = {};
|
|
1453
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1454
|
+
const next = rewrite(value, inJob);
|
|
1455
|
+
if (!sameValue(next, value)) set[key] = next;
|
|
1456
|
+
}
|
|
1457
|
+
return set;
|
|
1458
|
+
};
|
|
1459
|
+
const jobs = workflow ? asRecord(workflow["jobs"]) : { [job]: { steps } };
|
|
1460
|
+
return Object.entries(jobs).flatMap(([name, value]) => {
|
|
1461
|
+
const { steps: own = [], ...fields } = asRecord(value);
|
|
1462
|
+
const inJob = name === job;
|
|
1463
|
+
const jobSet = changed(fields, inJob);
|
|
1464
|
+
return [...Object.keys(jobSet).length > 0 ? [{
|
|
1465
|
+
type: "updateJob",
|
|
1466
|
+
job: name,
|
|
1467
|
+
set: jobSet
|
|
1468
|
+
}] : [], ...(Array.isArray(own) ? own : []).flatMap((step, i) => {
|
|
1469
|
+
const set = inJob && i === index ? {} : changed(asRecord(step), inJob);
|
|
1470
|
+
return Object.keys(set).length > 0 ? [{
|
|
1471
|
+
type: "updateStep",
|
|
1472
|
+
job: name,
|
|
1473
|
+
index: i,
|
|
1474
|
+
set
|
|
1475
|
+
}] : [];
|
|
1476
|
+
})];
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function applyAll(editing, text, edits) {
|
|
1480
|
+
return edits.reduce((yml, edit) => editing.applyGraphEdit({
|
|
1481
|
+
yml,
|
|
1482
|
+
layout: void 0
|
|
1483
|
+
}, edit).yml, text);
|
|
1484
|
+
}
|
|
1485
|
+
function renamedJob(edit) {
|
|
1486
|
+
if (!edit) return;
|
|
1487
|
+
if (edit.type === "batch") return edit.edits.map(renamedJob).findLast(Boolean);
|
|
1488
|
+
return edit.type === "updateJob" ? edit.name : void 0;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* The linter's problems inside one job or step of `whole`, the workflow as the dialog has it,
|
|
1492
|
+
* with lines in `fragment`, the YAML the dialog shows, when it shows YAML.
|
|
1493
|
+
*/
|
|
1494
|
+
function useScopedProblems(whole, prefix, fragment, exclude = []) {
|
|
1495
|
+
const ready = useLintEditing();
|
|
1496
|
+
const context = useLintContext(whole);
|
|
1497
|
+
const contextKey = JSON.stringify(context);
|
|
1498
|
+
const scope = prefix.join(".");
|
|
1499
|
+
return useMemo(() => {
|
|
1500
|
+
if (!ready || whole === void 0) return NO_SCOPED;
|
|
1501
|
+
const problems = ready.lintWorkflow(whole, context).flatMap((problem) => {
|
|
1502
|
+
const path = problem.path.split(".");
|
|
1503
|
+
if (!prefix.every((segment, i) => path[i] === segment) || exclude.includes(path[prefix.length] ?? "")) return [];
|
|
1504
|
+
return [{
|
|
1505
|
+
at: path.slice(prefix.length).map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment),
|
|
1506
|
+
message: problem.message,
|
|
1507
|
+
fix: problem.fix
|
|
1508
|
+
}];
|
|
1509
|
+
});
|
|
1510
|
+
return {
|
|
1511
|
+
problems,
|
|
1512
|
+
markers: fragment === null ? [] : problems.map((problem) => ({
|
|
1513
|
+
message: problem.message,
|
|
1514
|
+
line: (problem.at.length > 0 ? ready.linesAt(fragment, problem.at)?.start : void 0) ?? 1
|
|
1515
|
+
}))
|
|
1516
|
+
};
|
|
1517
|
+
}, [
|
|
1518
|
+
ready,
|
|
1519
|
+
whole,
|
|
1520
|
+
contextKey,
|
|
1521
|
+
scope,
|
|
1522
|
+
fragment,
|
|
1523
|
+
exclude.join()
|
|
1524
|
+
]);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* The input groups a `with:` completion in the YAML view adds to the workflow, saved with the
|
|
1528
|
+
* dialog ahead of the text that reads them.
|
|
1529
|
+
*/
|
|
1530
|
+
function useAddedGroups(start, committed) {
|
|
1531
|
+
const editing = useWorkflowEditing();
|
|
1532
|
+
const [added, setAdded] = useState([]);
|
|
1533
|
+
const nested = {
|
|
1534
|
+
inputs: () => {
|
|
1535
|
+
const text = editedWorkflow(editing, start, committed).yml;
|
|
1536
|
+
const on = asRecord(asRecord(text === void 0 ? {} : editing.loadYaml(text))["on"]);
|
|
1537
|
+
return {
|
|
1538
|
+
...asRecord(asRecord(on["execute"])["inputs"]),
|
|
1539
|
+
...Object.fromEntries(added.map((group) => [group.name, group.definition]))
|
|
1540
|
+
};
|
|
1541
|
+
},
|
|
1542
|
+
addInputs: (name, definition) => setAdded((current) => [...current.filter((group) => group.name !== name), {
|
|
1543
|
+
name,
|
|
1544
|
+
definition
|
|
1545
|
+
}])
|
|
1546
|
+
};
|
|
1547
|
+
const edits = (text) => added.filter(({ name }) => text.includes(`inputs.${name}.`)).map(({ name, definition }) => ({
|
|
1548
|
+
type: "addInput",
|
|
1549
|
+
parent: [],
|
|
1550
|
+
index: 0,
|
|
1551
|
+
name,
|
|
1552
|
+
definition
|
|
1553
|
+
}));
|
|
1554
|
+
return {
|
|
1555
|
+
nested,
|
|
1556
|
+
edits,
|
|
1557
|
+
clear: () => setAdded([])
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function editedWorkflow(editing, start, edits) {
|
|
1561
|
+
if (start === void 0) return {
|
|
1562
|
+
yml: void 0,
|
|
1563
|
+
error: void 0
|
|
1564
|
+
};
|
|
1565
|
+
try {
|
|
1566
|
+
return {
|
|
1567
|
+
yml: applyAll(editing, start, edits),
|
|
1568
|
+
error: void 0
|
|
1569
|
+
};
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
return {
|
|
1572
|
+
yml: void 0,
|
|
1573
|
+
error
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
/** A dialog's settings in a form or as YAML, with the edits carried across a switch. */
|
|
1578
|
+
function useSettingsViews(o) {
|
|
1579
|
+
const t = useGraphEditorStrings();
|
|
1580
|
+
const editing = useWorkflowEditing();
|
|
1581
|
+
const sections = useSectionMemory();
|
|
1582
|
+
const [start] = useState(o.source);
|
|
1583
|
+
const [opening] = useState(() => openingYaml(o.view, o.source, (text) => o.read(text, o.name)));
|
|
1584
|
+
const [committed, setCommitted] = useState([]);
|
|
1585
|
+
const [name, setName] = useState(o.name);
|
|
1586
|
+
const [yamlName, setYamlName] = useState(o.name);
|
|
1587
|
+
const [yaml, setYaml] = useState(opening);
|
|
1588
|
+
const [yamlAtStart, setYamlAtStart] = useState(opening ?? "");
|
|
1589
|
+
const [problem, setProblem] = useState();
|
|
1590
|
+
const [version, setVersion] = useState(0);
|
|
1591
|
+
const draft = useRef(null);
|
|
1592
|
+
const invalidDraft = useRef(false);
|
|
1593
|
+
const [, setDrafts] = useState(0);
|
|
1594
|
+
const lastDraft = useRef("");
|
|
1595
|
+
const trackDraft = (edit, invalid) => {
|
|
1596
|
+
draft.current = edit;
|
|
1597
|
+
invalidDraft.current = invalid;
|
|
1598
|
+
const seen = JSON.stringify([edit, invalid]);
|
|
1599
|
+
if (seen !== lastDraft.current) {
|
|
1600
|
+
lastDraft.current = seen;
|
|
1601
|
+
window.setTimeout(() => {
|
|
1602
|
+
setDrafts((n) => n + 1);
|
|
1603
|
+
if (!invalid) setProblem(void 0);
|
|
1604
|
+
}, 0);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const groups = useAddedGroups(start, committed);
|
|
1608
|
+
const drafted = () => draft.current ? [...committed, draft.current] : committed;
|
|
1609
|
+
const withYaml = (text) => text === yamlAtStart ? committed : [
|
|
1610
|
+
...committed,
|
|
1611
|
+
...groups.edits(text),
|
|
1612
|
+
o.write(name, text)
|
|
1613
|
+
];
|
|
1614
|
+
const problemOf = (text) => o.optional && !text.trim() ? void 0 : yamlProblem(text, t, editing);
|
|
1615
|
+
const nextName = yamlName.trim();
|
|
1616
|
+
const nameError = o.rename?.error(nextName, name);
|
|
1617
|
+
const withRename = (edits) => o.rename && nextName !== name ? [...edits, o.rename.edit(name, nextName)] : edits;
|
|
1618
|
+
const edits = yaml !== null ? withYaml(yaml) : drafted();
|
|
1619
|
+
const editsKey = JSON.stringify(edits);
|
|
1620
|
+
const applied = useMemo(() => editedWorkflow(editing, start, edits), [
|
|
1621
|
+
editing,
|
|
1622
|
+
start,
|
|
1623
|
+
editsKey
|
|
1624
|
+
]);
|
|
1625
|
+
const whole = applied.yml;
|
|
1626
|
+
const refused = yaml !== null && applied.error !== void 0 ? editErrorText(t, editing, applied.error) : void 0;
|
|
1627
|
+
const scopeName = yaml !== null ? name : o.rename?.from(draft.current) ?? name;
|
|
1628
|
+
const scoped = useScopedProblems(whole, o.scope(scopeName), yaml, o.exclude);
|
|
1629
|
+
const current = useMemo(() => start !== void 0 && committed.length > 0 ? asRecord(editing.loadYaml(applyAll(editing, start, committed))) : null, [
|
|
1630
|
+
editing,
|
|
1631
|
+
start,
|
|
1632
|
+
committed
|
|
1633
|
+
]);
|
|
1634
|
+
const save = (edits) => {
|
|
1635
|
+
const edit = editing.batchOf(o.addition ? [o.addition, ...edits] : edits);
|
|
1636
|
+
if (edit) o.onEdit(edit);
|
|
1637
|
+
o.onClose();
|
|
1638
|
+
};
|
|
1639
|
+
const toYaml = () => {
|
|
1640
|
+
if (start === void 0) return;
|
|
1641
|
+
if (invalidDraft.current) {
|
|
1642
|
+
setProblem(t.fixFieldsFirst);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const edits = drafted();
|
|
1646
|
+
const renamed = o.rename?.from(draft.current) ?? name;
|
|
1647
|
+
try {
|
|
1648
|
+
const text = o.read(applyAll(editing, start, edits), renamed);
|
|
1649
|
+
setCommitted(edits);
|
|
1650
|
+
setName(renamed);
|
|
1651
|
+
setYamlName(renamed);
|
|
1652
|
+
setYaml(text);
|
|
1653
|
+
setYamlAtStart(text);
|
|
1654
|
+
setProblem(void 0);
|
|
1655
|
+
o.onViewChange?.("yaml");
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
setProblem(editErrorText(t, editing, error));
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
const toForm = () => {
|
|
1661
|
+
if (yaml === null) return;
|
|
1662
|
+
const issue = problemOf(yaml) ?? nameError ?? refused;
|
|
1663
|
+
if (issue) {
|
|
1664
|
+
setProblem(issue);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
setCommitted(withRename(withYaml(yaml)));
|
|
1668
|
+
setName(nextName);
|
|
1669
|
+
groups.clear();
|
|
1670
|
+
setYaml(null);
|
|
1671
|
+
setVersion((current) => current + 1);
|
|
1672
|
+
o.onViewChange?.("form");
|
|
1673
|
+
};
|
|
1674
|
+
const switcher = /* @__PURE__ */ jsx(ViewSwitch, {
|
|
1675
|
+
yaml: yaml !== null,
|
|
1676
|
+
onForm: toForm,
|
|
1677
|
+
onYaml: toYaml
|
|
1678
|
+
});
|
|
1679
|
+
const yamlDialog = (title, path, text) => /* @__PURE__ */ jsxs(DialogShell, {
|
|
1680
|
+
title,
|
|
1681
|
+
onClose: o.onClose,
|
|
1682
|
+
dirty: committed.length > 0 || text !== yamlAtStart || nextName !== name,
|
|
1683
|
+
saveDisabled: problemOf(text) !== void 0 || nameError !== void 0 || refused !== void 0,
|
|
1684
|
+
onSubmit: () => save(withRename(withYaml(text))),
|
|
1685
|
+
headerEnd: switcher,
|
|
1686
|
+
openOnAdd: o.openOnAdd,
|
|
1687
|
+
children: [o.rename && /* @__PURE__ */ jsx(Input, {
|
|
1688
|
+
mono: true,
|
|
1689
|
+
label: o.rename.label,
|
|
1690
|
+
description: o.rename.description,
|
|
1691
|
+
value: yamlName,
|
|
1692
|
+
error: nameError,
|
|
1693
|
+
onChange: (e) => setYamlName(e.target.value)
|
|
1694
|
+
}), /* @__PURE__ */ jsx(YamlPane, {
|
|
1695
|
+
path,
|
|
1696
|
+
nested: groups.nested,
|
|
1697
|
+
value: text,
|
|
1698
|
+
onChange: (next) => {
|
|
1699
|
+
setYaml(next);
|
|
1700
|
+
setProblem(void 0);
|
|
1701
|
+
},
|
|
1702
|
+
problem: problem ?? problemOf(text) ?? refused,
|
|
1703
|
+
scoped
|
|
1704
|
+
})]
|
|
1705
|
+
});
|
|
1706
|
+
const formDialog = (render) => /* @__PURE__ */ jsx(SectionMemory.Provider, {
|
|
1707
|
+
value: sections,
|
|
1708
|
+
children: /* @__PURE__ */ jsx(FieldProblems, {
|
|
1709
|
+
problems: scoped.problems,
|
|
1710
|
+
children: render({
|
|
1711
|
+
pending: committed.length > 0 || o.addition !== void 0,
|
|
1712
|
+
onDraft: trackDraft,
|
|
1713
|
+
renderShell: (shellProps) => /* @__PURE__ */ jsxs(DialogShell, {
|
|
1714
|
+
...shellProps,
|
|
1715
|
+
headerEnd: switcher,
|
|
1716
|
+
openOnAdd: o.openOnAdd,
|
|
1717
|
+
onSubmit: () => save(drafted()),
|
|
1718
|
+
children: [
|
|
1719
|
+
/* @__PURE__ */ jsx(FieldError, { message: problem }),
|
|
1720
|
+
/* @__PURE__ */ jsx(ProblemList, { problems: scoped.problems }),
|
|
1721
|
+
shellProps.children
|
|
1722
|
+
]
|
|
1723
|
+
})
|
|
1724
|
+
})
|
|
1725
|
+
})
|
|
1726
|
+
});
|
|
1727
|
+
return {
|
|
1728
|
+
start,
|
|
1729
|
+
yaml,
|
|
1730
|
+
name,
|
|
1731
|
+
version,
|
|
1732
|
+
current,
|
|
1733
|
+
yamlDialog,
|
|
1734
|
+
formDialog
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
/** A job's settings in a form, or as YAML; switching carries the edits across. */
|
|
1738
|
+
function JobDialog(props) {
|
|
1739
|
+
const t = useGraphEditorStrings();
|
|
1740
|
+
const editing = useWorkflowEditing();
|
|
1741
|
+
const views = useSettingsViews({
|
|
1742
|
+
...props,
|
|
1743
|
+
name: props.job,
|
|
1744
|
+
read: editing.jobYaml,
|
|
1745
|
+
write: (job, yaml) => ({
|
|
1746
|
+
type: "setJobYaml",
|
|
1747
|
+
job,
|
|
1748
|
+
yaml
|
|
1749
|
+
}),
|
|
1750
|
+
scope: (job) => ["jobs", job],
|
|
1751
|
+
rename: {
|
|
1752
|
+
label: t.jobName,
|
|
1753
|
+
description: t.help.jobName,
|
|
1754
|
+
error: (next, current) => !editing.isValidJobName(next) ? t.invalidJobName : next !== current && next !== props.job && Object.hasOwn(props.jobs, next) ? t.jobExists : void 0,
|
|
1755
|
+
edit: (job, name) => ({
|
|
1756
|
+
type: "updateJob",
|
|
1757
|
+
job,
|
|
1758
|
+
name
|
|
1759
|
+
}),
|
|
1760
|
+
from: renamedJob
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
if (views.start === void 0) return /* @__PURE__ */ jsx(JobForm, { ...props });
|
|
1764
|
+
if (views.yaml !== null) return views.yamlDialog(t.editJob, JOB_YAML_PATH, views.yaml);
|
|
1765
|
+
const { current } = views;
|
|
1766
|
+
return views.formDialog((host) => /* @__PURE__ */ jsx(JobForm, {
|
|
1767
|
+
...props,
|
|
1768
|
+
...host,
|
|
1769
|
+
job: views.name,
|
|
1770
|
+
jobs: current ? asRecord(current["jobs"]) : props.jobs,
|
|
1771
|
+
workflow: current ?? props.workflow
|
|
1772
|
+
}, views.version));
|
|
1773
|
+
}
|
|
1774
|
+
/** A step's settings in a form, or as YAML; switching carries the edits across. */
|
|
1775
|
+
function StepDialog(props) {
|
|
1776
|
+
const { job, index } = props;
|
|
1777
|
+
const t = useGraphEditorStrings();
|
|
1778
|
+
const editing = useWorkflowEditing();
|
|
1779
|
+
const views = useSettingsViews({
|
|
1780
|
+
...props,
|
|
1781
|
+
name: job,
|
|
1782
|
+
read: (text) => editing.stepYaml(text, job, index),
|
|
1783
|
+
write: (_, yaml) => ({
|
|
1784
|
+
type: "setStepYaml",
|
|
1785
|
+
job,
|
|
1786
|
+
index,
|
|
1787
|
+
yaml
|
|
1788
|
+
}),
|
|
1789
|
+
scope: () => [
|
|
1790
|
+
"jobs",
|
|
1791
|
+
job,
|
|
1792
|
+
"steps",
|
|
1793
|
+
String(index)
|
|
1794
|
+
]
|
|
1795
|
+
});
|
|
1796
|
+
if (views.start === void 0) return /* @__PURE__ */ jsx(StepForm, { ...props });
|
|
1797
|
+
if (views.yaml !== null) return views.yamlDialog(t.editStep, STEP_YAML_PATH, views.yaml);
|
|
1798
|
+
const steps = asRecord(asRecord(views.current?.["jobs"])[job])["steps"];
|
|
1799
|
+
return views.formDialog((host) => /* @__PURE__ */ jsx(StepForm, {
|
|
1800
|
+
...props,
|
|
1801
|
+
...host,
|
|
1802
|
+
adding: props.addition !== void 0,
|
|
1803
|
+
steps: Array.isArray(steps) ? steps : props.steps,
|
|
1804
|
+
workflow: views.current ?? props.workflow
|
|
1805
|
+
}, views.version));
|
|
1806
|
+
}
|
|
1807
|
+
//#endregion
|
|
1808
|
+
export { JOB_FIELDS, JobDialog, RETRY_FIELDS, RUNS_ON_FIELDS, SSH_KEYS, STEP_FIELDS, STRATEGY_FIELDS, StepDialog, useSettingsViews };
|