@parallelworks/ui 0.74.0 → 0.75.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 +9 -1
- package/dist/ai/adapter/types.d.ts +5 -1
- package/dist/ai/components/agent/AgentMessageParts.js +41 -2
- package/dist/ai/core/parts.js +4 -0
- package/dist/ai/strings.d.ts +10 -0
- package/dist/ai/strings.js +9 -0
- package/dist/ai/types.d.ts +12 -0
- package/dist/components/Provider.d.ts +3 -0
- package/dist/components/Provider.js +2 -1
- package/dist/graph/DependencyGraph.d.ts +4 -0
- package/dist/graph/DependencyGraph.js +125 -3
- package/dist/graph/InputsEditor.js +1 -1
- package/dist/graph/JobSummary.d.ts +2 -0
- package/dist/graph/JobSummary.js +19 -5
- package/dist/graph/MatrixGroup.js +28 -12
- package/dist/graph/RunWarningsList.d.ts +19 -0
- package/dist/graph/RunWarningsList.js +94 -0
- package/dist/graph/TreeView.js +16 -5
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/runWarnings.d.ts +58 -0
- package/dist/graph/runWarnings.js +84 -0
- package/dist/graph/viewport.d.ts +19 -0
- package/dist/graph/viewport.js +62 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
99
99
|
| A live relative time or duration | `RelativeTime`, `Elapsed` |
|
|
100
100
|
| Rendered Markdown | `Markdown` |
|
|
101
101
|
| Code or YAML to read or edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
|
|
102
|
-
| A run's jobs, steps, logs and
|
|
102
|
+
| A run's jobs, steps, logs, annotations and warnings | `DependencyGraph` from `./graph`, as a graph or a list |
|
|
103
103
|
| A workflow's jobs drawn from its YAML, or edited by hand | `DependencyGraphPreview` from `./graph`, editable with an `editor`; `InputsFormEditor` for its run form |
|
|
104
104
|
| A value that survives a reload | `usePersistedState` |
|
|
105
105
|
| Choosing the app's colors | `ThemeEditor`, applied with `useApplyTheme` |
|
|
@@ -775,6 +775,14 @@ highlight changes, and panning, zooming and a running run's clock re-render none
|
|
|
775
775
|
the whole graph, zooms out to 1% to show the shape of the largest, and in to 200% to read a node;
|
|
776
776
|
the wheel zooms by the same factor at every scale, about the pointer.
|
|
777
777
|
|
|
778
|
+
A run's `warnings` (`RunWarning`: a `message` about a `job`, or about one of its steps by `step`
|
|
779
|
+
index, in the subworkflow its `path` of `[job, 'step_<n>', …]` names) are listed over the graph,
|
|
780
|
+
each linking to its job or step: the link opens the subworkflows, matrix and job around it, brings
|
|
781
|
+
it and what it is in into view below the graph's view bar, readable, and moves focus to it. Each job and step a warning is about, and each one it sits
|
|
782
|
+
inside, carries a mark that lists the warnings on hover, so one skipped without a log is still
|
|
783
|
+
flagged. A job name a matrix ran as several jobs marks each of them. `dag.warningCount` words the
|
|
784
|
+
count.
|
|
785
|
+
|
|
778
786
|
The editor checks YAML against the schemas a host registers with `configureEditorYaml` from
|
|
779
787
|
`@parallelworks/ui/editor`. The job, step, input and settings dialogs edit their YAML at their own
|
|
780
788
|
paths (`JOB_YAML_PATH`, `STEP_YAML_PATH`, `INPUT_YAML_PATH`, `SETTINGS_YAML_PATH`); `settingsSchemas`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
|
|
1
|
+
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, ToolDecision, UnreachableSession } from '../types.js';
|
|
2
2
|
import type { CompletionMessage } from './openai/wire.js';
|
|
3
3
|
export declare class ChatAdapterError extends Error {
|
|
4
4
|
status?: number | undefined;
|
|
@@ -48,6 +48,10 @@ export type PartDelta = {
|
|
|
48
48
|
id: string;
|
|
49
49
|
result: string;
|
|
50
50
|
isError?: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'tool_decision';
|
|
53
|
+
id: string;
|
|
54
|
+
decision: ToolDecision;
|
|
51
55
|
} | {
|
|
52
56
|
type: 'subagent_start';
|
|
53
57
|
id: string;
|
|
@@ -7,6 +7,7 @@ import { fieldBox } from "../../../components/fieldBox.js";
|
|
|
7
7
|
import { focusOnMount } from "../../../components/focus.js";
|
|
8
8
|
import { Disclosure } from "../../../components/Disclosure.js";
|
|
9
9
|
import { formatDuration } from "../../../format.js";
|
|
10
|
+
import { RelativeTime } from "../../../components/relativeTime.js";
|
|
10
11
|
import { LogViewer } from "../../../logviewer/LogViewer.js";
|
|
11
12
|
import { agentColorHex, bashExitCode, fullToolArgs, isGroupableTool, rollupSentence, summarizeToolArgs, toolHeaderFor, toolSummary } from "../../agent/toolfmt.js";
|
|
12
13
|
import { computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, parseEditArgs } from "../../agent/diff.js";
|
|
@@ -277,7 +278,45 @@ function ToolCallBlock({ part }) {
|
|
|
277
278
|
isDiff && /* @__PURE__ */ jsx("div", {
|
|
278
279
|
className: "ml-5",
|
|
279
280
|
children: /* @__PURE__ */ jsx(EditDiffBlock, { part })
|
|
280
|
-
})
|
|
281
|
+
}),
|
|
282
|
+
part.decision && /* @__PURE__ */ jsx(DecisionLine, { decision: part.decision })
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function decisionLabel(d, t) {
|
|
287
|
+
switch (d.status) {
|
|
288
|
+
case "approved":
|
|
289
|
+
if (d.by === "auto-reviewer") return t.allowedByReviewer;
|
|
290
|
+
return d.name ? t.allowedBy(d.name) : t.allowed;
|
|
291
|
+
case "denied":
|
|
292
|
+
if (d.by === "auto-reviewer") return t.deniedByReviewer;
|
|
293
|
+
return d.name ? t.deniedBy(d.name) : t.denied;
|
|
294
|
+
case "expired": return t.approvalExpired;
|
|
295
|
+
case "timed_out": return t.approvalTimedOut;
|
|
296
|
+
case "failed": return t.reviewFailed;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function DecisionLine({ decision }) {
|
|
300
|
+
const { strings } = useChatConfig();
|
|
301
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
302
|
+
className: "ml-5 mt-0.5 flex min-w-0 flex-wrap items-baseline gap-x-1 text-xs theme-muted-text",
|
|
303
|
+
"data-testid": "tool-decision",
|
|
304
|
+
children: [
|
|
305
|
+
/* @__PURE__ */ jsx("span", { children: decisionLabel(decision, strings.agentTranscript) }),
|
|
306
|
+
decision.at && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
307
|
+
"aria-hidden": "true",
|
|
308
|
+
children: "·"
|
|
309
|
+
}), /* @__PURE__ */ jsx(RelativeTime, {
|
|
310
|
+
value: decision.at,
|
|
311
|
+
className: "shrink-0"
|
|
312
|
+
})] }),
|
|
313
|
+
decision.note && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
314
|
+
"aria-hidden": "true",
|
|
315
|
+
children: "·"
|
|
316
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
317
|
+
className: "min-w-0 max-w-full truncate",
|
|
318
|
+
children: decision.note
|
|
319
|
+
})] })
|
|
281
320
|
]
|
|
282
321
|
});
|
|
283
322
|
}
|
|
@@ -749,7 +788,7 @@ function buildRenderItems(parts) {
|
|
|
749
788
|
run = [];
|
|
750
789
|
};
|
|
751
790
|
parts.forEach((part, index) => {
|
|
752
|
-
if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && isGroupableTool(part.name)) {
|
|
791
|
+
if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && !part.decision && isGroupableTool(part.name)) {
|
|
753
792
|
run.push({
|
|
754
793
|
part,
|
|
755
794
|
index
|
package/dist/ai/core/parts.js
CHANGED
|
@@ -39,6 +39,10 @@ function applyPartDelta(parts, delta) {
|
|
|
39
39
|
status: delta.isError ? "error" : "ok",
|
|
40
40
|
result: delta.result
|
|
41
41
|
} : p);
|
|
42
|
+
case "tool_decision": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
|
|
43
|
+
...p,
|
|
44
|
+
decision: delta.decision
|
|
45
|
+
} : p);
|
|
42
46
|
case "subagent_start": {
|
|
43
47
|
const part = {
|
|
44
48
|
kind: "subagent",
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -234,6 +234,16 @@ export interface ChatStrings {
|
|
|
234
234
|
denyWithFeedback: string;
|
|
235
235
|
feedbackPlaceholder: string;
|
|
236
236
|
answered: string;
|
|
237
|
+
/** Under a call someone answered: who allowed or denied it. */
|
|
238
|
+
allowedBy: (name: string) => string;
|
|
239
|
+
deniedBy: (name: string) => string;
|
|
240
|
+
allowed: string;
|
|
241
|
+
denied: string;
|
|
242
|
+
allowedByReviewer: string;
|
|
243
|
+
deniedByReviewer: string;
|
|
244
|
+
approvalExpired: string;
|
|
245
|
+
approvalTimedOut: string;
|
|
246
|
+
reviewFailed: string;
|
|
237
247
|
writeOwnAnswer: string;
|
|
238
248
|
subagent: string;
|
|
239
249
|
tasks: string;
|
package/dist/ai/strings.js
CHANGED
|
@@ -253,6 +253,15 @@ var defaultChatStrings = {
|
|
|
253
253
|
denyWithFeedback: "No, and provide feedback to the agent",
|
|
254
254
|
feedbackPlaceholder: "Tell the agent what to do instead…",
|
|
255
255
|
answered: "Answered",
|
|
256
|
+
allowedBy: (name) => `Allowed by ${name}`,
|
|
257
|
+
deniedBy: (name) => `Denied by ${name}`,
|
|
258
|
+
allowed: "Allowed",
|
|
259
|
+
denied: "Denied",
|
|
260
|
+
allowedByReviewer: "Allowed by the auto-reviewer",
|
|
261
|
+
deniedByReviewer: "Denied by the auto-reviewer",
|
|
262
|
+
approvalExpired: "Expired without an answer",
|
|
263
|
+
approvalTimedOut: "No answer in time",
|
|
264
|
+
reviewFailed: "The auto-reviewer could not decide",
|
|
256
265
|
writeOwnAnswer: "Write my own answer…",
|
|
257
266
|
subagent: "Task",
|
|
258
267
|
tasks: "Tasks",
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -55,6 +55,17 @@ export interface ReasoningPart {
|
|
|
55
55
|
text: string;
|
|
56
56
|
durationMs?: number;
|
|
57
57
|
}
|
|
58
|
+
export type ToolDecisionStatus = 'approved' | 'denied' | 'expired' | 'timed_out' | 'failed';
|
|
59
|
+
/** How a call that needed an answer was settled. */
|
|
60
|
+
export interface ToolDecision {
|
|
61
|
+
status: ToolDecisionStatus;
|
|
62
|
+
by: 'user' | 'auto-reviewer';
|
|
63
|
+
/** The person who answered, when `by` is a user. */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** ISO time of the answer. */
|
|
66
|
+
at?: string;
|
|
67
|
+
note?: string;
|
|
68
|
+
}
|
|
58
69
|
export interface ToolCallPart {
|
|
59
70
|
kind: 'tool_call';
|
|
60
71
|
id: string;
|
|
@@ -63,6 +74,7 @@ export interface ToolCallPart {
|
|
|
63
74
|
status: ToolCallStatus;
|
|
64
75
|
result?: string;
|
|
65
76
|
startLine?: number;
|
|
77
|
+
decision?: ToolDecision;
|
|
66
78
|
}
|
|
67
79
|
export type SubagentStatus = 'running' | 'done' | 'failed';
|
|
68
80
|
export interface SubagentPart {
|
|
@@ -268,6 +268,9 @@ export interface UIStrings {
|
|
|
268
268
|
annotationSource: (job: string, step: string) => string;
|
|
269
269
|
/** A step without a name, numbered from 0 as the run reports it. */
|
|
270
270
|
stepNumber: (index: number) => string;
|
|
271
|
+
/** How many warnings: the title of a run's list of warnings, and the name of the mark on a job
|
|
272
|
+
* or step they're about, such as "2 warnings". */
|
|
273
|
+
warningCount: (count: number) => string;
|
|
271
274
|
};
|
|
272
275
|
jobActions: {
|
|
273
276
|
openSubworkflowLogs: string;
|
|
@@ -281,7 +281,8 @@ var DEFAULTS = {
|
|
|
281
281
|
]
|
|
282
282
|
].filter(([count]) => count).map(([count, one, many]) => `${count} ${count === 1 ? one : many}`).join(", ")}`,
|
|
283
283
|
annotationSource: (job, step) => `in ${job} → ${step}`,
|
|
284
|
-
stepNumber: (index) => `step ${index}
|
|
284
|
+
stepNumber: (index) => `step ${index}`,
|
|
285
|
+
warningCount: (count) => count === 1 ? "1 warning" : `${count} warnings`
|
|
285
286
|
},
|
|
286
287
|
jobActions: {
|
|
287
288
|
openSubworkflowLogs: "Open subworkflow logs",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GraphLayout } from '../editing.js';
|
|
2
2
|
import type { DependencyGraphEditor } from './editorApi.js';
|
|
3
|
+
import { type RunWarning } from './runWarnings.js';
|
|
3
4
|
import { type RunLink, type WorkflowJob } from './types.js';
|
|
4
5
|
export type ViewMode = 'dag' | 'tree';
|
|
5
6
|
export declare const addCleanupSteps: ({ jobs }: {
|
|
@@ -16,6 +17,9 @@ interface GraphRun {
|
|
|
16
17
|
completedAt?: string | undefined;
|
|
17
18
|
executedJobs?: Record<string, unknown> | undefined;
|
|
18
19
|
links?: Record<string, RunLink> | undefined;
|
|
20
|
+
/** What is probably wrong with the run's jobs and steps as written, listed over the graph and
|
|
21
|
+
* marked on each job and step they're about, which matters most for one skipped without a log. */
|
|
22
|
+
warnings?: readonly RunWarning[] | null | undefined;
|
|
19
23
|
}
|
|
20
24
|
export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar, }: {
|
|
21
25
|
run: GraphRun;
|
|
@@ -14,12 +14,15 @@ import { useStickyTop } from "../components/stickyTop.js";
|
|
|
14
14
|
import { Tabs } from "../components/Tabs.js";
|
|
15
15
|
import { LogViewer } from "../logviewer/LogViewer.js";
|
|
16
16
|
import { initializeValues } from "../form/lib.js";
|
|
17
|
+
import { prefersReducedMotion } from "../reducedMotion.js";
|
|
17
18
|
import { AnnotationBanner } from "./AnnotationBanner.js";
|
|
18
19
|
import { jobLabel, toggled } from "./util.js";
|
|
19
20
|
import { EDITOR_HANDLE_CLASS, GraphEditorProvider, useGraphEditor, useSelectedEdges } from "./graphEditorState.js";
|
|
20
21
|
import { EdgeEnds, EdgeHitPath, EmptyGraphEditor, GraphEditorCanvas, NodePorts } from "./editorRows.js";
|
|
21
22
|
import { emptyColumnsStyle } from "./gridSpacing.js";
|
|
22
23
|
import { Reveal } from "./Reveal.js";
|
|
24
|
+
import { RunWarningsList } from "./RunWarningsList.js";
|
|
25
|
+
import { WarningMarks, WarningMarksContext, locateWarnings, rowKey } from "./runWarnings.js";
|
|
23
26
|
import { Joblist } from "./JobSummary.js";
|
|
24
27
|
import { Store, useStore } from "./store.js";
|
|
25
28
|
import { Lineage, LineageContext } from "./lineage.js";
|
|
@@ -32,7 +35,7 @@ import { MatrixGroupNode, MatrixGroupSummaryItem } from "./MatrixGroup.js";
|
|
|
32
35
|
import { StepSummaries } from "./StepSummaries.js";
|
|
33
36
|
import { StepSummary } from "./StepSummary.js";
|
|
34
37
|
import TreeView from "./TreeView.js";
|
|
35
|
-
import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
|
|
38
|
+
import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoView, viewShowing, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
|
|
36
39
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
37
40
|
import { Fragment as Fragment$1, Suspense, createContext, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
38
41
|
import { flushSync } from "react-dom";
|
|
@@ -224,6 +227,7 @@ function jobHandlers({ engine, jobs, displayJobs, pathPrefix, stepsOpen, toggleS
|
|
|
224
227
|
const openLog = (j, s) => setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
|
|
225
228
|
const openSummary = (j, s) => setSublogOpen(fileBeside(engine.stepLogPath(pathPrefix, j, s), "summary.md"));
|
|
226
229
|
return {
|
|
230
|
+
pathPrefix,
|
|
227
231
|
isStepsOpen,
|
|
228
232
|
onJobClick: (j) => {
|
|
229
233
|
const status = jobs[j]?.status ?? displayJobs[j]?.status;
|
|
@@ -700,6 +704,43 @@ function graphBounds(content) {
|
|
|
700
704
|
}
|
|
701
705
|
return bounds;
|
|
702
706
|
}
|
|
707
|
+
var READABLE_SCALE = .5;
|
|
708
|
+
/** Where a job or step a warning took the graph to shows: the panel below its view bar, which sits
|
|
709
|
+
* over the graph. */
|
|
710
|
+
function visibleArea(panel, bar) {
|
|
711
|
+
const outer = panel.getBoundingClientRect();
|
|
712
|
+
const top = outer.top + panel.clientTop;
|
|
713
|
+
const below = bar ? Math.max(top, bar.getBoundingClientRect().bottom) : top;
|
|
714
|
+
return {
|
|
715
|
+
left: outer.left + panel.clientLeft,
|
|
716
|
+
top: below,
|
|
717
|
+
width: panel.clientWidth,
|
|
718
|
+
height: Math.max(0, top + panel.clientHeight - below)
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function flash(row) {
|
|
722
|
+
const tone = getComputedStyle(row).getPropertyValue("--theme-warning").trim();
|
|
723
|
+
if (!tone || typeof row.animate !== "function") return;
|
|
724
|
+
const ring = {
|
|
725
|
+
outline: `3px solid ${tone}`,
|
|
726
|
+
outlineOffset: "4px",
|
|
727
|
+
borderRadius: "6px"
|
|
728
|
+
};
|
|
729
|
+
row.animate([
|
|
730
|
+
ring,
|
|
731
|
+
{
|
|
732
|
+
...ring,
|
|
733
|
+
offset: .7
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
...ring,
|
|
737
|
+
outlineColor: "transparent"
|
|
738
|
+
}
|
|
739
|
+
], {
|
|
740
|
+
duration: 2e3,
|
|
741
|
+
easing: "ease-in"
|
|
742
|
+
});
|
|
743
|
+
}
|
|
703
744
|
function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar }) {
|
|
704
745
|
const engine = useWorkflowEngine();
|
|
705
746
|
const animT = .125;
|
|
@@ -708,6 +749,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
708
749
|
const runSlug = run.slug ?? "";
|
|
709
750
|
const workflowName = run.workflowName ?? "";
|
|
710
751
|
const runStatus = toStatus(run.status);
|
|
752
|
+
const runWarnings = run.warnings;
|
|
753
|
+
const warnings = useMemo(() => runWarnings?.length ? locateWarnings(runWarnings, rootJobs) : [], [runWarnings, rootJobs]);
|
|
754
|
+
const freshMarks = useMemo(() => warnings.length ? new WarningMarks(warnings) : WarningMarks.EMPTY, [warnings]);
|
|
755
|
+
const [marks, setMarks] = useState(freshMarks);
|
|
756
|
+
if (marks !== freshMarks && marks.signature !== freshMarks.signature) setMarks(freshMarks);
|
|
711
757
|
const [sublogOpen, setSublogOpen] = useState("");
|
|
712
758
|
const [stepsOpen, setStepsOpen] = useState(/* @__PURE__ */ new Set());
|
|
713
759
|
const [expandedSubworkflows, setExpandedSubworkflows] = useState(/* @__PURE__ */ new Set());
|
|
@@ -901,8 +947,14 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
901
947
|
]);
|
|
902
948
|
const { dependencyCols } = rootGraph;
|
|
903
949
|
const held = useRef(null);
|
|
950
|
+
const shown = useRef(null);
|
|
951
|
+
const viewBarRef = useRef(null);
|
|
904
952
|
const viewed = useRef(false);
|
|
905
953
|
const rowOf = useCallback((job) => [...containerRef.current?.querySelectorAll("[data-dag-job]") ?? []].find((el) => el.dataset["dagJob"] === job) ?? null, []);
|
|
954
|
+
const runRowOf = useCallback((key, jobKey) => {
|
|
955
|
+
const rows = [...containerRef.current?.querySelectorAll("[data-run-row]") ?? []];
|
|
956
|
+
return rows.find((el) => el.dataset["runRow"] === key) ?? (jobKey === void 0 ? null : rows.find((el) => el.dataset["runRow"] === jobKey) ?? null);
|
|
957
|
+
}, []);
|
|
906
958
|
const nodeOf = useCallback((job) => {
|
|
907
959
|
const row = rowOf(job);
|
|
908
960
|
return row?.closest("[id^=\"node_\"]") ?? row;
|
|
@@ -927,6 +979,18 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
927
979
|
const transform = transformRef.current;
|
|
928
980
|
const bounds = content ? graphBounds(content) : null;
|
|
929
981
|
if (!container || !content || !transform || !bounds) return;
|
|
982
|
+
const showing = shown.current && performance.now() - shown.current.at < HOLD_MS ? shown.current.keys.flatMap((key) => runRowOf(key) ?? []) : [];
|
|
983
|
+
const [row] = showing;
|
|
984
|
+
if (row) {
|
|
985
|
+
const { positionX, positionY, scale } = transform.instance.state;
|
|
986
|
+
const view = viewShowing(enclosing(showing.map((el) => el.getBoundingClientRect())), row.getBoundingClientRect(), visibleArea(container, viewBarRef.current), (transform.instance.wrapperComponent ?? container).getBoundingClientRect(), {
|
|
987
|
+
x: positionX,
|
|
988
|
+
y: positionY,
|
|
989
|
+
scale
|
|
990
|
+
}, READABLE_SCALE);
|
|
991
|
+
if (view) transform.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
930
994
|
const panel = {
|
|
931
995
|
width: container.clientWidth,
|
|
932
996
|
height: container.clientHeight
|
|
@@ -956,7 +1020,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
956
1020
|
const wrapperEl = container.querySelector(".react-transform-component");
|
|
957
1021
|
if (wrapperEl) wrapperEl.style.transform = `translate(${view.x}px, ${view.y}px) scale(${view.scale})`;
|
|
958
1022
|
requestAnimationFrame(() => transform.setTransform(view.x, view.y, view.scale, 0));
|
|
959
|
-
}, [
|
|
1023
|
+
}, [
|
|
1024
|
+
nodeOf,
|
|
1025
|
+
editor,
|
|
1026
|
+
runRowOf
|
|
1027
|
+
]);
|
|
960
1028
|
const fitWhenSettled = useCallback(async () => {
|
|
961
1029
|
const container = containerRef.current;
|
|
962
1030
|
const content = graphContentRef.current;
|
|
@@ -989,6 +1057,51 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
989
1057
|
const pos = offsetWithin(el, content);
|
|
990
1058
|
transform.setTransform(container.clientWidth / 2 - (pos.x + el.offsetWidth / 2) * scale, container.clientHeight / 2 - (pos.y + el.offsetHeight / 2) * scale, scale, FIT_ANIMATION_MS, "easeOut");
|
|
991
1059
|
}, []);
|
|
1060
|
+
const canShowWarnings = viewMode !== "tree" || !!setViewMode;
|
|
1061
|
+
const showWarning = (target) => {
|
|
1062
|
+
const elsewhere = !target.prefix.startsWith(pathAppend);
|
|
1063
|
+
const via = elsewhere ? target.via : target.via.filter((v) => v.prefix.startsWith(pathAppend));
|
|
1064
|
+
const jobKeys = via.map((v) => rowKey(v.prefix, v.job));
|
|
1065
|
+
if (target.step !== void 0) jobKeys.push(rowKey(target.prefix, target.job));
|
|
1066
|
+
const matrixKeys = [...via, target].flatMap(({ prefix, job }) => {
|
|
1067
|
+
const origin = walkJobPath(rootJobs, parsePrefixToPath(prefix))[job]?._matrix?.originaljob;
|
|
1068
|
+
return origin ? [`${prefix}${origin}`] : [];
|
|
1069
|
+
});
|
|
1070
|
+
shown.current = {
|
|
1071
|
+
keys: [
|
|
1072
|
+
target.key,
|
|
1073
|
+
rowKey(target.prefix, target.job),
|
|
1074
|
+
...via.flatMap((v) => [rowKey(v.prefix, v.job, v.step), rowKey(v.prefix, v.job)]).reverse()
|
|
1075
|
+
],
|
|
1076
|
+
at: performance.now()
|
|
1077
|
+
};
|
|
1078
|
+
flushSync(() => {
|
|
1079
|
+
if (viewMode === "tree") setViewMode?.("dag");
|
|
1080
|
+
if (elsewhere) {
|
|
1081
|
+
resetGraph();
|
|
1082
|
+
setExecutedJobsPath((prev) => [[], ...prev.slice(executedJobsPathIdx, 10)]);
|
|
1083
|
+
setExecutedJobsPathIdx(0);
|
|
1084
|
+
}
|
|
1085
|
+
setStepsOpen((prev) => /* @__PURE__ */ new Set([...prev, ...jobKeys]));
|
|
1086
|
+
setExpandedSubworkflows((prev) => /* @__PURE__ */ new Set([...prev, ...via.map((v) => `${v.prefix}${v.job}:${v.step}`)]));
|
|
1087
|
+
setMatrixOpen((prev) => /* @__PURE__ */ new Set([...prev, ...matrixKeys]));
|
|
1088
|
+
});
|
|
1089
|
+
const row = runRowOf(target.key, rowKey(target.prefix, target.job));
|
|
1090
|
+
if (row) {
|
|
1091
|
+
row.focus({ preventScroll: true });
|
|
1092
|
+
flash(row);
|
|
1093
|
+
}
|
|
1094
|
+
const first = fitViewRef.current;
|
|
1095
|
+
if (first && first.scale < READABLE_SCALE) fitViewRef.current = {
|
|
1096
|
+
...first,
|
|
1097
|
+
scale: READABLE_SCALE
|
|
1098
|
+
};
|
|
1099
|
+
containerRef.current?.scrollIntoView({
|
|
1100
|
+
block: "nearest",
|
|
1101
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
1102
|
+
});
|
|
1103
|
+
fitWhenSettled();
|
|
1104
|
+
};
|
|
992
1105
|
const registerShow = editor?.registerShow;
|
|
993
1106
|
useEffect(() => {
|
|
994
1107
|
if (!editorApi) return;
|
|
@@ -1363,6 +1476,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
1363
1476
|
})
|
|
1364
1477
|
]
|
|
1365
1478
|
}),
|
|
1479
|
+
!preview && /* @__PURE__ */ jsx(RunWarningsList, {
|
|
1480
|
+
warnings,
|
|
1481
|
+
rootJobs,
|
|
1482
|
+
onShow: canShowWarnings ? showWarning : void 0
|
|
1483
|
+
}),
|
|
1366
1484
|
!preview && /* @__PURE__ */ jsx(AnnotationBanner, { executedJobs: jobs }),
|
|
1367
1485
|
viewMode === "tree" && !preview ? /* @__PURE__ */ jsx("div", {
|
|
1368
1486
|
className: "panel w-full border",
|
|
@@ -1396,6 +1514,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
1396
1514
|
onPinch: moved,
|
|
1397
1515
|
onTransform,
|
|
1398
1516
|
children: () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
1517
|
+
ref: viewBarRef,
|
|
1399
1518
|
className: "absolute z-10 top-2 right-2 flex max-w-[calc(100%-1rem)] flex-col items-end gap-y-1",
|
|
1400
1519
|
children: /* @__PURE__ */ jsx(GraphViewControls, {
|
|
1401
1520
|
history: executedJobsPath.length > 1 ? {
|
|
@@ -1498,7 +1617,10 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
|
|
|
1498
1617
|
});
|
|
1499
1618
|
return /* @__PURE__ */ jsx(GraphEditorProvider, {
|
|
1500
1619
|
value: editorApi,
|
|
1501
|
-
children:
|
|
1620
|
+
children: /* @__PURE__ */ jsx(WarningMarksContext, {
|
|
1621
|
+
value: marks,
|
|
1622
|
+
children: graph
|
|
1623
|
+
})
|
|
1502
1624
|
});
|
|
1503
1625
|
}
|
|
1504
1626
|
/** Jobs as a run evaluates them before it starts: each `strategy`, and a `needs` expression, from the inputs' defaults. */
|
|
@@ -7,8 +7,8 @@ import { menuDivider } from "../components/MenuEntries.js";
|
|
|
7
7
|
import { IconButton } from "../components/IconButton.js";
|
|
8
8
|
import { useContextMenu } from "../components/ContextMenu.js";
|
|
9
9
|
import { NumberInput } from "../components/NumberInput.js";
|
|
10
|
-
import { MarqueeBox, createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore } from "./editorPrimitives.js";
|
|
11
10
|
import { prefersReducedMotion } from "../reducedMotion.js";
|
|
11
|
+
import { MarqueeBox, createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore } from "./editorPrimitives.js";
|
|
12
12
|
import { useGraphEditorStrings, useInputsEditorStrings } from "./editorStrings.js";
|
|
13
13
|
import { asRecord } from "./records.js";
|
|
14
14
|
import { openOnAddOf, text } from "./editorFields.js";
|
|
@@ -2,6 +2,8 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { WorkflowJob } from './types.js';
|
|
3
3
|
/** What a job list does when its jobs and steps are clicked, at one graph level. */
|
|
4
4
|
export interface JobHandlers {
|
|
5
|
+
/** The level's `subworkflows/<job>/step_<n>/` chain, empty at the run's own jobs. */
|
|
6
|
+
pathPrefix: string;
|
|
5
7
|
isStepsOpen: (jobName: string) => boolean;
|
|
6
8
|
onJobClick: (jobName: string) => void;
|
|
7
9
|
onStepClick: (jobName: string, stepIndex: number) => void;
|
package/dist/graph/JobSummary.js
CHANGED
|
@@ -6,6 +6,8 @@ import { RunStatusIcon } from "../components/RunStatusIcon.js";
|
|
|
6
6
|
import { jobLabel } from "./util.js";
|
|
7
7
|
import { AddStepButton, EditableJobRow, EditableStepRow } from "./editorRows.js";
|
|
8
8
|
import { Reveal } from "./Reveal.js";
|
|
9
|
+
import { WarningMark } from "./RunWarningsList.js";
|
|
10
|
+
import { rowKey, useWarningMarks } from "./runWarnings.js";
|
|
9
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
12
|
//#region src/graph/JobSummary.tsx
|
|
11
13
|
function UsesIcon({ uses, subworkflow }) {
|
|
@@ -32,6 +34,8 @@ function UsesIcon({ uses, subworkflow }) {
|
|
|
32
34
|
function Joblist(inputs) {
|
|
33
35
|
const { dag, jobActions } = useStrings();
|
|
34
36
|
const engine = useWorkflowEngine();
|
|
37
|
+
const marks = useWarningMarks();
|
|
38
|
+
const prefix = inputs.pathPrefix;
|
|
35
39
|
return inputs.jobNames.map((jobName) => {
|
|
36
40
|
if (inputs.jobs[jobName]?._matrixGroup) return null;
|
|
37
41
|
const job = inputs.jobs[jobName];
|
|
@@ -40,12 +44,17 @@ function Joblist(inputs) {
|
|
|
40
44
|
const toggle = /* @__PURE__ */ jsxs("button", {
|
|
41
45
|
type: "button",
|
|
42
46
|
"aria-expanded": inputs.isStepsOpen(jobName),
|
|
47
|
+
"data-run-row": rowKey(prefix, jobName),
|
|
43
48
|
onClick: () => inputs.onJobClick(jobName),
|
|
44
49
|
className: cx("flex items-center gap-x-1.5 w-full text-left", jobStatus === "skipped" || jobStatus === "skipped-failed" ? "cursor-default" : "cursor-pointer"),
|
|
45
|
-
children: [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
children: [
|
|
51
|
+
!inputs.preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: jobStatus }),
|
|
52
|
+
/* @__PURE__ */ jsx("div", {
|
|
53
|
+
className: "font-medium",
|
|
54
|
+
children: label
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName)) })
|
|
57
|
+
]
|
|
49
58
|
});
|
|
50
59
|
return /* @__PURE__ */ jsxs("div", {
|
|
51
60
|
className: "py-1 select-none",
|
|
@@ -65,10 +74,15 @@ function Joblist(inputs) {
|
|
|
65
74
|
const canExpand = !!subJobs && Object.keys(subJobs).length > 0;
|
|
66
75
|
const stepButton = /* @__PURE__ */ jsxs("button", {
|
|
67
76
|
type: "button",
|
|
77
|
+
"data-run-row": rowKey(prefix, jobName, i),
|
|
68
78
|
onClick: () => inputs.onStepClick(jobName, i),
|
|
69
79
|
className: cx(inputs.preview ? "ml-1.5" : "ml-4", "gap-x-1.5 flex items-center text-left"),
|
|
70
80
|
"data-testid": stepLabel,
|
|
71
|
-
children: [
|
|
81
|
+
children: [
|
|
82
|
+
inputs.preview ? "↳" : /* @__PURE__ */ jsx(RunStatusIcon, { status: step.status }),
|
|
83
|
+
/* @__PURE__ */ jsx("div", { children: stepLabel }),
|
|
84
|
+
/* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName, i)) })
|
|
85
|
+
]
|
|
72
86
|
});
|
|
73
87
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
74
88
|
className: cx("flex items-center", step.status === "skipped" || step.status === "skipped-failed" ? "cursor-default" : "cursor-pointer"),
|
|
@@ -2,14 +2,21 @@ import { useStrings, useWorkflowEngine } from "../components/Provider.js";
|
|
|
2
2
|
import { RunStatusIcon } from "../components/RunStatusIcon.js";
|
|
3
3
|
import { EditableJobRow } from "./editorRows.js";
|
|
4
4
|
import { Reveal } from "./Reveal.js";
|
|
5
|
+
import { WarningMark } from "./RunWarningsList.js";
|
|
6
|
+
import { rowKey, useWarningMarks } from "./runWarnings.js";
|
|
5
7
|
import { Joblist } from "./JobSummary.js";
|
|
6
8
|
import { GraphNode } from "./GraphNode.js";
|
|
7
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
10
|
import { memo } from "react";
|
|
9
11
|
//#region src/graph/MatrixGroup.tsx
|
|
12
|
+
/** The warnings about any of a matrix's runs, for its group's row while it is closed. */
|
|
13
|
+
function useMatrixWarnings(prefix, members) {
|
|
14
|
+
return useWarningMarks().underAny(members.map((member) => rowKey(prefix, member)));
|
|
15
|
+
}
|
|
10
16
|
/** Matrix group item in the summary sidebar. */
|
|
11
17
|
function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, jobs, handlers, preview }) {
|
|
12
18
|
const { dag: t } = useStrings();
|
|
19
|
+
const warnings = useMatrixWarnings(handlers.pathPrefix, matrixGroup.members);
|
|
13
20
|
return /* @__PURE__ */ jsxs("div", {
|
|
14
21
|
className: "py-1.5",
|
|
15
22
|
children: [/* @__PURE__ */ jsxs("button", {
|
|
@@ -17,17 +24,21 @@ function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, job
|
|
|
17
24
|
"aria-expanded": isExpanded,
|
|
18
25
|
onClick: onToggle,
|
|
19
26
|
className: "cursor-pointer flex items-center gap-x-1.5 w-full text-left",
|
|
20
|
-
children: [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
className: "font-
|
|
24
|
-
children: [
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(RunStatusIcon, { status }),
|
|
29
|
+
/* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: "font-semibold",
|
|
31
|
+
children: [t.matrixOf(matrixGroup.originaljob), /* @__PURE__ */ jsxs("span", {
|
|
32
|
+
className: "font-normal text-sm ml-1",
|
|
33
|
+
children: [
|
|
34
|
+
"(",
|
|
35
|
+
t.jobCount(matrixGroup.members.length),
|
|
36
|
+
")"
|
|
37
|
+
]
|
|
38
|
+
})]
|
|
39
|
+
}),
|
|
40
|
+
!isExpanded && /* @__PURE__ */ jsx(WarningMark, { warnings })
|
|
41
|
+
]
|
|
31
42
|
}), /* @__PURE__ */ jsx(Reveal, {
|
|
32
43
|
open: isExpanded,
|
|
33
44
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -49,6 +60,7 @@ var MatrixGroupNode = memo(function MatrixGroupNode({ matrixGroup, jobNames, idP
|
|
|
49
60
|
const first = jobNames[0] ?? "";
|
|
50
61
|
const matrixMembers = matrixGroup.members;
|
|
51
62
|
const { aggStatus, statusLabel } = useWorkflowEngine().matrixStatus(matrixMembers.map((m) => jobs[m]?.status));
|
|
63
|
+
const warnings = useMatrixWarnings(idPrefix, matrixMembers);
|
|
52
64
|
const header = /* @__PURE__ */ jsxs("button", {
|
|
53
65
|
type: "button",
|
|
54
66
|
"aria-expanded": isExpanded,
|
|
@@ -65,7 +77,11 @@ var MatrixGroupNode = memo(function MatrixGroupNode({ matrixGroup, jobNames, idP
|
|
|
65
77
|
})] }) : t.matrixOf(matrixName)
|
|
66
78
|
}), !isExpanded && /* @__PURE__ */ jsxs("div", {
|
|
67
79
|
className: "mt-1 border-t border-(--theme-border) pt-1 flex items-center gap-x-1",
|
|
68
|
-
children: [
|
|
80
|
+
children: [
|
|
81
|
+
!preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: aggStatus }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { children: preview ? t.jobCount(matrixMembers.length) : statusLabel }),
|
|
83
|
+
/* @__PURE__ */ jsx(WarningMark, { warnings })
|
|
84
|
+
]
|
|
69
85
|
})]
|
|
70
86
|
});
|
|
71
87
|
return /* @__PURE__ */ jsxs(GraphNode, {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LocatedWarning, RunWarning, WarningTarget } from './runWarnings.js';
|
|
2
|
+
import type { WorkflowJob } from './types.js';
|
|
3
|
+
/** A warning's message, with the text it puts in backticks, such as a keyword, set as code. */
|
|
4
|
+
export declare function WarningText({ message }: {
|
|
5
|
+
message: string;
|
|
6
|
+
}): string | (string | import("react").JSX.Element)[];
|
|
7
|
+
/** Flags a job or step that warnings are about, or that holds something they're about; hovering
|
|
8
|
+
* it lists them. */
|
|
9
|
+
export declare function WarningMark({ warnings, className, }: {
|
|
10
|
+
warnings: readonly RunWarning[];
|
|
11
|
+
className?: string | undefined;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
13
|
+
/** The run's warnings, each with a link to the job or step it's about. */
|
|
14
|
+
export declare function RunWarningsList({ warnings, rootJobs, onShow, }: {
|
|
15
|
+
warnings: readonly LocatedWarning[];
|
|
16
|
+
rootJobs: Record<string, WorkflowJob>;
|
|
17
|
+
/** Brings a warning's job or step into view; without it, where each one is reads as text. */
|
|
18
|
+
onShow?: ((target: WarningTarget) => void) | undefined;
|
|
19
|
+
}): import("react").JSX.Element | null;
|