@parallelworks/ui 0.39.0 → 0.40.1

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.
@@ -18,15 +18,22 @@ import { AnnotationBanner } from "./AnnotationBanner.js";
18
18
  import { Collapse } from "./Collapse.js";
19
19
  import { jobLabel, toggled } from "./util.js";
20
20
  import { EDITOR_HANDLE_CLASS, GraphEditorProvider, useGraphEditor, useSelectedEdges } from "./graphEditorState.js";
21
- import { BoxGrip, EdgeEnds, EdgeHitPath, EmptyGraphEditor, GraphEditorCanvas, NodePorts } from "./editorRows.js";
22
- import { emptyColumnsStyle, emptyRowsStyle } from "./gridSpacing.js";
21
+ import { EdgeEnds, EdgeHitPath, EmptyGraphEditor, GraphEditorCanvas, NodePorts } from "./editorRows.js";
22
+ import { emptyColumnsStyle } from "./gridSpacing.js";
23
23
  import { Reveal } from "./Reveal.js";
24
24
  import { Joblist } from "./JobSummary.js";
25
+ import { Store, useStore } from "./store.js";
26
+ import { Lineage, LineageContext } from "./lineage.js";
27
+ import { DimRules, JobNode, setDimmed } from "./GraphNode.js";
28
+ import { ConnectorLayers } from "./Connectors.js";
29
+ import { routeConnectors, sameBoxes } from "./connectorGeometry.js";
30
+ import { isJobRecord } from "./types.js";
31
+ import { computeGraphLayout, displayJobsOf, hopDistances, positionsOf } from "./graphLayout.js";
25
32
  import { MatrixGroupNode, MatrixGroupSummaryItem } from "./MatrixGroup.js";
26
33
  import TreeView from "./TreeView.js";
27
- import { isJobRecord } from "./types.js";
34
+ import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
28
35
  import cx from "classnames";
29
- import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
36
+ import { Fragment, Suspense, createContext, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
30
37
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
31
38
  import { flushSync } from "react-dom";
32
39
  import { DateTime } from "luxon";
@@ -199,171 +206,6 @@ function parsePrefixToPath(prefix) {
199
206
  }
200
207
  return path;
201
208
  }
202
- function unitsOnly(columns, together) {
203
- return columns.map((col) => {
204
- const boxes = [];
205
- const rows = [];
206
- let last = -1;
207
- col.boxes.forEach((box, i) => {
208
- const groups = [];
209
- for (const job of box) {
210
- const group = groups.find((g) => together(g[0] ?? job, job));
211
- if (group) group.push(job);
212
- else groups.push([job]);
213
- }
214
- const main = groups.reduce((a, b) => b.length > a.length ? b : a);
215
- let row = Math.max(col.rows[i] ?? i, last + 1);
216
- for (const group of [main, ...groups.filter((g) => g !== main)]) {
217
- boxes.push(group);
218
- rows.push(row);
219
- last = row;
220
- row += 1;
221
- }
222
- });
223
- return {
224
- ...col,
225
- boxes,
226
- rows
227
- };
228
- });
229
- }
230
- function withLoneMatrixSlots({ layoutPosition, mapLayout }, layout, jobs, matrixGroups) {
231
- const lone = Object.entries(jobs).flatMap(([name, job]) => {
232
- const original = job._matrix?.originaljob;
233
- const slot = original && !matrixGroups[original] ? layoutPosition(layout, original) : void 0;
234
- return slot && !layoutPosition(layout, name) ? [{
235
- name,
236
- slot
237
- }] : [];
238
- });
239
- if (lone.length === 0) return layout;
240
- const next = mapLayout(layout, (position) => position);
241
- for (const { name, slot } of lone) next[name] = slot;
242
- return next;
243
- }
244
- function displayJobsOf(engine, jobs, matrixGroups) {
245
- const visibleJobs = structuredClone(jobs);
246
- for (const jobName of Object.keys(jobs)) if (jobs[jobName]?.if === false) delete visibleJobs[jobName];
247
- engine.collapseMatrixGroups(matrixGroups, visibleJobs);
248
- return visibleJobs;
249
- }
250
- function computeGraphLayout(engine, jobs, matrixGroups, layout) {
251
- const visibleJobs = displayJobsOf(engine, jobs, matrixGroups);
252
- const allDeps = {};
253
- for (const jobName of Object.keys(visibleJobs)) allDeps[jobName] = engine.jobDependencies(jobName, visibleJobs, allDeps);
254
- const filteredDeps = {};
255
- for (const jobName of Object.keys(visibleJobs)) {
256
- const needs = visibleJobs[jobName]?.needs;
257
- const oldNeeds = Array.isArray(needs) ? [...new Set(needs.map((need) => typeof need === "string" && engine.editing ? engine.editing.needTarget(need) : need))] : [];
258
- filteredDeps[jobName] = oldNeeds.filter((dep) => {
259
- return !oldNeeds.some((other) => {
260
- if (other === dep) return false;
261
- return allDeps[other]?.has(dep) === true;
262
- });
263
- }).sort();
264
- }
265
- const depended_on = {};
266
- for (const jobName of Object.keys(visibleJobs)) for (const dep of filteredDeps[jobName] ?? []) {
267
- depended_on[dep] ??= [];
268
- depended_on[dep].push(jobName);
269
- }
270
- for (const dependents of Object.values(depended_on)) dependents.sort();
271
- const unfinished = new Set(Object.keys(visibleJobs));
272
- const rootLevel = [];
273
- const deps = [rootLevel];
274
- for (const jobName of unfinished) if (filteredDeps[jobName]?.length === 0) {
275
- rootLevel.push(jobName);
276
- unfinished.delete(jobName);
277
- }
278
- let prevLen;
279
- do {
280
- const toAppend = [];
281
- const pending = new Set(unfinished);
282
- prevLen = pending.size;
283
- for (const jobName of pending) if ((filteredDeps[jobName] ?? []).every((dep) => !pending.has(dep))) {
284
- toAppend.push(jobName);
285
- unfinished.delete(jobName);
286
- }
287
- if (toAppend.length > 0) deps.push(toAppend);
288
- } while (prevLen !== unfinished.size);
289
- const together = (head, jobName) => {
290
- const depsA = depended_on[head];
291
- const depsB = depended_on[jobName];
292
- const depsA2 = filteredDeps[head];
293
- const depsB2 = filteredDeps[jobName];
294
- return (!depsA && !depsB || !!depsA && !!depsB && depsA.length === depsB.length) && (!depsA2 && !depsB2 || !!depsA2 && !!depsB2 && depsA2.length === depsB2.length) && (!depsA || depsA.every((dep, i) => dep === depsB?.[i])) && (!depsA2 || depsA2.every((dep, i) => dep === depsB2?.[i]));
295
- };
296
- const alone = (job) => !!visibleJobs[job]?._matrixGroup;
297
- const editing = engine.editing;
298
- const laidOut = layout && editing?.hasLayout(layout) ? unitsOnly(editing.boxesFromLayout(Object.keys(visibleJobs), filteredDeps, withLoneMatrixSlots(editing, layout, visibleJobs, matrixGroups), {
299
- together,
300
- alone
301
- }), together) : null;
302
- const finalDeps = laidOut ? laidOut.map((col) => col.boxes) : [];
303
- if (!laidOut) deps.forEach((col) => {
304
- const toAppend = [];
305
- while (col.length > 0) {
306
- const head = col[0];
307
- if (head === void 0) break;
308
- const grouped = [head];
309
- const toRm = [0];
310
- col.slice(1).forEach((jobName, j) => {
311
- if (!alone(head) && !alone(jobName) && together(head, jobName)) {
312
- grouped.push(jobName);
313
- toRm.push(j + 1);
314
- }
315
- });
316
- col.splice(0, col.length, ...col.filter((_, i) => !toRm.includes(i)));
317
- toAppend.push(grouped);
318
- }
319
- finalDeps.push(toAppend);
320
- });
321
- if (finalDeps[0]?.length === 0) {
322
- finalDeps.shift();
323
- laidOut?.shift();
324
- }
325
- const ancestorDists = {};
326
- const descendantDists = {};
327
- const jobNames = Object.keys(visibleJobs);
328
- const reverseDeps = {};
329
- for (const [job, depList] of Object.entries(filteredDeps)) for (const dep of depList) {
330
- if (!reverseDeps[dep]) reverseDeps[dep] = [];
331
- reverseDeps[dep].push(job);
332
- }
333
- for (const start of jobNames) {
334
- const aDist = /* @__PURE__ */ new Map();
335
- const q1 = [[start, 0]];
336
- for (let next = q1.shift(); next; next = q1.shift()) {
337
- const [job, d] = next;
338
- for (const dep of filteredDeps[job] ?? []) if (!aDist.has(dep)) {
339
- aDist.set(dep, d + 1);
340
- q1.push([dep, d + 1]);
341
- }
342
- }
343
- ancestorDists[start] = aDist;
344
- const dDist = /* @__PURE__ */ new Map();
345
- const q2 = [[start, 0]];
346
- for (let next = q2.shift(); next; next = q2.shift()) {
347
- const [job, d] = next;
348
- for (const child of reverseDeps[job] ?? []) if (!dDist.has(child)) {
349
- dDist.set(child, d + 1);
350
- q2.push([child, d + 1]);
351
- }
352
- }
353
- descendantDists[start] = dDist;
354
- }
355
- return {
356
- dependencyCols: finalDeps,
357
- /** Each box's row; above a box, rows the layout leaves empty stay as space. */
358
- rowSlots: laidOut ? laidOut.map((col) => col.rows) : finalDeps.map((col) => col.map((_, i) => i)),
359
- /** Each column's number; between two drawn columns, numbers skipped stay as space. */
360
- colSlots: laidOut ? laidOut.map((col) => col.column) : finalDeps.map((_, i) => i),
361
- directDeps: filteredDeps,
362
- displayJobs: visibleJobs,
363
- ancestorDists,
364
- descendantDists
365
- };
366
- }
367
209
  function jobHandlers({ engine, jobs, displayJobs, pathPrefix, stepsOpen, toggleSteps, expandedSubworkflows, toggleSubworkflow, openInNewGraph, setSublogOpen, renderWell, editStep }) {
368
210
  const isStepsOpen = (j) => stepsOpen.has(pathPrefix + j);
369
211
  const subworkflowKey = (j, s) => `${pathPrefix}${j}:${s}`;
@@ -407,10 +249,13 @@ function SubworkflowWell({ expanded, onSettled, className, children }) {
407
249
  });
408
250
  }
409
251
  var SUBGRAPH_Z_STEP = 100;
252
+ var FADE_NODES = 300;
410
253
  var GraphEditorOverlays = lazy(() => import("./GraphEditor.js"));
411
- var CONNECTOR_COLOR = "var(--theme-border)";
412
- var HIGHLIGHT_COLOR = "var(--theme-element)";
413
254
  var FIT_ANIMATION_MS = 300;
255
+ var ConnectorClock = class extends Store {
256
+ bump = () => this.notify();
257
+ };
258
+ var ConnectorClockContext = createContext(null);
414
259
  function sizesSettled(els) {
415
260
  return new Promise((resolve) => {
416
261
  let frame = 0;
@@ -429,33 +274,36 @@ function sizesSettled(els) {
429
274
  function sizingTransitions(el) {
430
275
  return el.getAnimations({ subtree: true }).filter((animation) => animation instanceof CSSTransition && (animation.transitionProperty === "grid-template-rows" || animation.transitionProperty === "width"));
431
276
  }
432
- function intoView(start, end, low, high) {
433
- if (end - start > high - low) return 0;
434
- if (start < low) return low - start;
435
- return end > high ? high - end : 0;
436
- }
437
277
  var HOLD_MS = 2e3;
438
- var ZOOM_STEP = .25;
439
- function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflows, toggleSubworkflow, openInNewGraph, setSublogOpen, invalidateConnectors, requestFit, stepsOpen, toggleSteps, matrixOpen, toggleMatrix, onReady, layout, editable = false }) {
278
+ var skippedBefore = (slots, i) => (slots?.[i] ?? i) - (i === 0 ? 0 : (slots?.[i - 1] ?? i - 1) + 1);
279
+ function measureBoxes(cols, pathPrefix, wrapper) {
280
+ const boxes = /* @__PURE__ */ new Map();
281
+ for (const col of cols) for (const [head] of col) {
282
+ const el = head === void 0 ? null : document.getElementById(`node_${pathPrefix}${head}`);
283
+ if (head !== void 0 && el) boxes.set(head, {
284
+ ...offsetWithin(el, wrapper),
285
+ width: el.offsetWidth
286
+ });
287
+ }
288
+ return boxes;
289
+ }
290
+ var Subgraph = memo(function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflows, toggleSubworkflow, openInNewGraph, setSublogOpen, invalidateConnectors, requestFit, stepsOpen, toggleSteps, matrixOpen, toggleMatrix, onReady, layout, graph, editable = false }) {
440
291
  const engine = useWorkflowEngine();
292
+ useStore(useContext(ConnectorClockContext), (clock) => clock.version, 0);
441
293
  const wrapperRef = useRef(null);
294
+ const routedRef = useRef(null);
442
295
  const editorApi = useGraphEditor();
443
296
  const selectedEdges = useSelectedEdges(editable ? editorApi : null);
444
297
  const zBase = depth * SUBGRAPH_Z_STEP;
445
- const [hoveredJob, setHoveredJob] = useState(null);
446
- const lastHoveredDistancesRef = useRef(null);
447
- const maxHoveredDistRef = useRef(0);
448
- const retractPathLensRef = useRef(/* @__PURE__ */ new Map());
449
- const matrixGroups = useMemo(() => engine.matrixGroups(jobs), [engine, jobs]);
450
298
  const placed = useMemo(() => layout ?? (depth > 0 ? positionsOf(jobs) : void 0), [
451
299
  layout,
452
300
  depth,
453
301
  jobs
454
302
  ]);
455
- const { dependencyCols, rowSlots, colSlots, directDeps, displayJobs, ancestorDists, descendantDists } = useMemo(() => computeGraphLayout(engine, jobs, matrixGroups, placed), [
303
+ const { dependencyCols, rowSlots, colSlots, directDeps, dependents, displayJobs } = useMemo(() => graph ?? computeGraphLayout(engine, jobs, engine.matrixGroups(jobs), placed), [
304
+ graph,
456
305
  engine,
457
306
  jobs,
458
- matrixGroups,
459
307
  placed
460
308
  ]);
461
309
  useEffect(() => {
@@ -475,43 +323,27 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
475
323
  rowSlots,
476
324
  colSlots
477
325
  ]);
478
- const hoveredDistances = useMemo(() => {
479
- if (!hoveredJob) return null;
480
- const dist = /* @__PURE__ */ new Map();
481
- dist.set(hoveredJob, 0);
482
- const ancestors = ancestorDists[hoveredJob];
483
- if (ancestors) for (const [k, v] of ancestors) dist.set(k, v);
484
- const descendants = descendantDists[hoveredJob];
485
- if (descendants) for (const [k, v] of descendants) dist.set(k, v);
486
- return dist;
487
- }, [
488
- hoveredJob,
489
- ancestorDists,
490
- descendantDists
326
+ const lineage = useMemo(() => new Lineage((job) => hopDistances(job, directDeps, dependents), {
327
+ animT,
328
+ depth,
329
+ fade: dependencyCols.reduce((n, col) => n + col.length, 0) <= FADE_NODES
330
+ }), [
331
+ directDeps,
332
+ dependents,
333
+ animT,
334
+ depth,
335
+ dependencyCols
491
336
  ]);
492
- if (hoveredDistances) {
493
- lastHoveredDistancesRef.current = hoveredDistances;
494
- maxHoveredDistRef.current = Math.max(...hoveredDistances.values());
495
- }
496
- const activeDists = hoveredDistances ?? lastHoveredDistancesRef.current;
497
337
  useEffect(() => {
498
- if (hoveredJob || !lastHoveredDistancesRef.current) return;
499
- const maxDelay = maxHoveredDistRef.current * animT + animT;
500
- const timer = setTimeout(() => {
501
- lastHoveredDistancesRef.current = null;
502
- maxHoveredDistRef.current = 0;
503
- invalidateConnectors();
504
- }, maxDelay * 1e3 + 200);
505
- return () => clearTimeout(timer);
506
- }, [
507
- hoveredJob,
508
- invalidateConnectors,
509
- animT
510
- ]);
511
- const hoveredRelated = useMemo(() => {
512
- if (!hoveredDistances) return null;
513
- return new Set(hoveredDistances.keys());
514
- }, [hoveredDistances]);
338
+ const wrapper = wrapperRef.current;
339
+ const dim = () => wrapper && setDimmed(wrapper, lineage.depth, lineage.hovering);
340
+ dim();
341
+ const unsubscribe = lineage.subscribe(dim);
342
+ return () => {
343
+ unsubscribe();
344
+ lineage.dispose();
345
+ };
346
+ }, [lineage]);
515
347
  useEffect(() => {
516
348
  const raf = requestAnimationFrame(() => {
517
349
  invalidateConnectors();
@@ -550,394 +382,207 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
550
382
  expandedSubworkflows,
551
383
  invalidateConnectors
552
384
  ]);
553
- const buildBoxLayer = useCallback(() => {
554
- const handlers = jobHandlers({
555
- engine,
556
- jobs,
557
- displayJobs,
558
- pathPrefix,
559
- stepsOpen,
560
- toggleSteps,
561
- expandedSubworkflows,
562
- toggleSubworkflow: (key) => {
563
- toggleSubworkflow(key);
564
- requestAnimationFrame(invalidateConnectors);
565
- },
566
- openInNewGraph,
567
- setSublogOpen,
568
- editStep: editable && editorApi ? (j, s) => editorApi.editStep(j, s) : void 0,
569
- renderWell: (subJobs, subPrefix, expanded) => /* @__PURE__ */ jsx(SubworkflowWell, {
570
- expanded,
571
- onSettled: requestFit,
572
- className: "my-2 rounded-lg border-4 border-(--theme-border)",
573
- children: /* @__PURE__ */ jsx(NestedSubgraph, {
574
- jobs: subJobs,
575
- pathPrefix: subPrefix,
576
- depth: depth + 1,
577
- preview,
578
- animT,
579
- expandedSubworkflows,
580
- toggleSubworkflow,
581
- openInNewGraph,
582
- setSublogOpen,
583
- invalidateConnectors,
584
- requestFit,
585
- stepsOpen,
586
- toggleSteps,
587
- matrixOpen,
588
- toggleMatrix
589
- })
385
+ const handlers = useMemo(() => jobHandlers({
386
+ engine,
387
+ jobs,
388
+ displayJobs,
389
+ pathPrefix,
390
+ stepsOpen,
391
+ toggleSteps,
392
+ expandedSubworkflows,
393
+ toggleSubworkflow: (key) => {
394
+ toggleSubworkflow(key);
395
+ requestAnimationFrame(invalidateConnectors);
396
+ },
397
+ openInNewGraph,
398
+ setSublogOpen,
399
+ editStep: editable && editorApi ? (j, s) => editorApi.editStep(j, s) : void 0,
400
+ renderWell: (subJobs, subPrefix, expanded) => /* @__PURE__ */ jsx(SubworkflowWell, {
401
+ expanded,
402
+ onSettled: requestFit,
403
+ className: "my-2 rounded-lg border-4 border-(--theme-border)",
404
+ children: /* @__PURE__ */ jsx(NestedSubgraph, {
405
+ jobs: subJobs,
406
+ pathPrefix: subPrefix,
407
+ depth: depth + 1,
408
+ preview,
409
+ animT,
410
+ expandedSubworkflows,
411
+ toggleSubworkflow,
412
+ openInNewGraph,
413
+ setSublogOpen,
414
+ invalidateConnectors,
415
+ requestFit,
416
+ stepsOpen,
417
+ toggleSteps,
418
+ matrixOpen,
419
+ toggleMatrix
590
420
  })
591
- });
592
- return /* @__PURE__ */ jsx("div", {
593
- className: "flex relative",
594
- children: dependencyCols.map((col, c) => {
595
- return /* @__PURE__ */ jsx("div", {
596
- style: emptyColumnsStyle((colSlots[c] ?? c) - (c === 0 ? 0 : (colSlots[c - 1] ?? c - 1) + 1)),
597
- children: col.map((jobNames, i) => {
598
- const first = jobNames[0];
599
- if (first === void 0) return null;
600
- const matrixGroup = displayJobs[first]?._matrixGroup;
601
- const rowsAbove = (rowSlots[c]?.[i] ?? i) - (i === 0 ? 0 : (rowSlots[c]?.[i - 1] ?? i - 1) + 1);
602
- if (matrixGroup) return /* @__PURE__ */ jsx(MatrixGroupNode, {
603
- matrixGroup,
604
- jobNames,
605
- idPrefix: pathPrefix,
606
- isExpanded: matrixOpen.has(pathPrefix + matrixGroup.originaljob),
607
- onToggle: () => toggleMatrix(pathPrefix + matrixGroup.originaljob),
608
- jobs,
609
- activeDists,
610
- onMouseEnter: () => setHoveredJob(first),
611
- onMouseLeave: () => setHoveredJob(null),
612
- hoveredRelated,
613
- animT,
614
- zBase,
615
- handlers,
616
- preview,
617
- editable,
618
- rowsAbove
619
- }, `node_${first}`);
620
- return /* @__PURE__ */ jsxs("div", {
621
- id: `node_${pathPrefix}${first}`,
622
- role: "none",
623
- className: "relative m-24",
624
- style: {
625
- zIndex: (activeDists?.has(first) ? 25 : 1) + zBase,
626
- ...emptyRowsStyle(rowsAbove)
627
- },
628
- onMouseEnter: () => setHoveredJob(first),
629
- onMouseLeave: () => setHoveredJob(null),
630
- onFocus: () => setHoveredJob(first),
631
- onBlur: () => setHoveredJob(null),
632
- children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-2xl bg-(--theme-panel-bg)" }), /* @__PURE__ */ jsxs("div", {
633
- className: "relative border-solid shadow py-4 px-8 rounded-xl border-4 whitespace-nowrap bg-(--theme-panel-bg) text-2xl",
634
- style: {
635
- opacity: hoveredRelated && !hoveredRelated.has(first) ? .5 : 1,
636
- transition: `opacity ${animT}s`
637
- },
638
- children: [editable && jobNames.length > 1 && /* @__PURE__ */ jsx(BoxGrip, { jobs: jobNames }), /* @__PURE__ */ jsx(Joblist, {
639
- jobs: displayJobs,
640
- jobNames,
641
- ...handlers,
642
- growWidth: true,
643
- preview,
644
- editable
645
- })]
646
- })]
647
- }, `node_${first}`);
648
- })
649
- }, col[0]?.[0]);
421
+ })
422
+ }), [
423
+ engine,
424
+ jobs,
425
+ displayJobs,
426
+ pathPrefix,
427
+ stepsOpen,
428
+ toggleSteps,
429
+ expandedSubworkflows,
430
+ toggleSubworkflow,
431
+ openInNewGraph,
432
+ setSublogOpen,
433
+ editable,
434
+ editorApi,
435
+ requestFit,
436
+ depth,
437
+ preview,
438
+ animT,
439
+ invalidateConnectors,
440
+ matrixOpen,
441
+ toggleMatrix
442
+ ]);
443
+ const nodes = useMemo(() => /* @__PURE__ */ jsx("div", {
444
+ className: "flex relative",
445
+ children: dependencyCols.map((col, c) => /* @__PURE__ */ jsx("div", {
446
+ style: emptyColumnsStyle(skippedBefore(colSlots, c)),
447
+ children: col.map((jobNames, i) => {
448
+ const first = jobNames[0];
449
+ if (first === void 0) return null;
450
+ const shell = {
451
+ rowsAbove: skippedBefore(rowSlots[c], i),
452
+ zBase
453
+ };
454
+ const matrixGroup = displayJobs[first]?._matrixGroup;
455
+ return matrixGroup ? /* @__PURE__ */ jsx(MatrixGroupNode, {
456
+ ...shell,
457
+ matrixGroup,
458
+ jobNames,
459
+ idPrefix: pathPrefix,
460
+ isExpanded: matrixOpen.has(pathPrefix + matrixGroup.originaljob),
461
+ toggleMatrix,
462
+ jobs,
463
+ handlers,
464
+ preview,
465
+ editable
466
+ }, `node_${first}`) : /* @__PURE__ */ jsx(JobNode, {
467
+ ...shell,
468
+ id: `node_${pathPrefix}${first}`,
469
+ jobNames,
470
+ jobs: displayJobs,
471
+ handlers,
472
+ preview,
473
+ editable
474
+ }, `node_${first}`);
650
475
  })
651
- });
652
- }, [
476
+ }, col[0]?.[0]))
477
+ }), [
653
478
  dependencyCols,
654
479
  rowSlots,
655
480
  colSlots,
656
481
  displayJobs,
657
482
  jobs,
658
- stepsOpen,
659
483
  matrixOpen,
660
- expandedSubworkflows,
661
- hoveredRelated,
662
- activeDists,
663
- animT,
664
484
  zBase,
665
485
  preview,
666
486
  pathPrefix,
667
- depth,
668
- toggleSteps,
669
- toggleSubworkflow,
670
487
  toggleMatrix,
671
- openInNewGraph,
672
- setSublogOpen,
673
- engine,
674
- invalidateConnectors,
675
- requestFit,
676
- editable,
677
- editorApi
488
+ handlers,
489
+ editable
678
490
  ]);
679
- const memoizedBoxLayer = useMemo(() => buildBoxLayer(), [buildBoxLayer]);
680
- const hasLiveNestedGraph = Array.from(expandedSubworkflows).some((k) => k.startsWith(pathPrefix));
681
491
  if (!dependencyCols[0]?.length) return null;
492
+ const wrapper = wrapperRef.current;
493
+ const measured = wrapper ? measureBoxes(dependencyCols, pathPrefix, wrapper) : /* @__PURE__ */ new Map();
494
+ const last = routedRef.current;
495
+ const routed = last && last.directDeps === directDeps && sameBoxes(last.boxes, measured) ? last : {
496
+ boxes: measured,
497
+ directDeps,
498
+ edges: routeConnectors(dependencyCols, colSlots.some((column, c) => column !== c), directDeps, measured, 37)
499
+ };
500
+ routedRef.current = routed;
501
+ const { boxes, edges } = routed;
682
502
  return /* @__PURE__ */ jsxs("div", {
683
503
  ref: wrapperRef,
684
504
  className: "relative",
685
- children: [
686
- (() => {
687
- const wrapper = wrapperRef.current;
688
- const gid = (name) => document.getElementById(`node_${pathPrefix}${name}`);
689
- const ox = (el) => offsetWithin(el, wrapper).x;
690
- const nodeConnectorY = (el) => offsetWithin(el, wrapper).y + 37;
691
- const depsOf = (head) => directDeps[head] ?? [];
692
- let colGap = 32;
693
- if (dependencyCols.length >= 2) {
694
- const col0 = gid(dependencyCols[0]?.[0]?.[0] ?? "");
695
- const col1 = gid(dependencyCols[1]?.[0]?.[0] ?? "");
696
- if (col0 && col1) colGap = ox(col1) - (ox(col0) + col0.offsetWidth);
697
- }
698
- if (colSlots.some((column, c) => column !== c)) for (let c = 1; c < dependencyCols.length; c++) {
699
- const left = gid(dependencyCols[c - 1]?.[0]?.[0] ?? "");
700
- const right = gid(dependencyCols[c]?.[0]?.[0] ?? "");
701
- if (left && right) colGap = Math.min(colGap, ox(right) - (ox(left) + left.offsetWidth));
702
- }
703
- let minAdy = Number.POSITIVE_INFINITY;
704
- for (const col of dependencyCols) for (const jobNames of col) {
705
- const first = jobNames[0];
706
- if (first === void 0) continue;
707
- for (const dep of depsOf(first)) {
708
- const s = gid(dep);
709
- const e = gid(first);
710
- if (!s || !e) continue;
711
- const dy = Math.abs(nodeConnectorY(e) - nodeConnectorY(s));
712
- if (dy > 0 && dy < minAdy) minAdy = dy;
713
- }
714
- }
715
- const MAX_ARC_R = 16;
716
- const arcR = Math.min(minAdy === Number.POSITIVE_INFINITY ? MAX_ARC_R : minAdy / 4, colGap / 2, MAX_ARC_R);
717
- const ports = (jobNames) => {
718
- const [head] = jobNames;
719
- const el = head === void 0 ? null : gid(head);
720
- if (head === void 0 || !el) return null;
721
- const y = nodeConnectorY(el);
722
- const wiredIn = depsOf(head).some((dep) => gid(dep));
723
- const wiredOut = dependencyCols.some((c) => c.some(([other = ""]) => depsOf(other).includes(head) && !!gid(other)));
724
- return /* @__PURE__ */ jsx(NodePorts, {
725
- jobs: jobNames,
726
- left: {
727
- x: ox(el) + 2,
728
- y
729
- },
730
- right: {
731
- x: ox(el) + el.offsetWidth - 2,
732
- y
733
- },
734
- drawLeft: !wiredIn,
735
- drawRight: !wiredOut,
736
- zIndex: 40 + zBase
737
- });
738
- };
739
- const hits = [];
740
- const connectors = dependencyCols.map((col) => /* @__PURE__ */ jsx(React.Fragment, { children: col.map((jobNames) => {
741
- const first = jobNames[0];
742
- if (first === void 0) return null;
743
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [editable && ports(jobNames), depsOf(first).map((dep) => {
744
- const isHighlighted = hoveredRelated?.has(first) && hoveredRelated?.has(dep);
745
- const depDist = activeDists?.get(dep) ?? 0;
746
- const targetDist = activeDists?.get(first) ?? 0;
747
- const closerDist = Math.min(depDist, targetDist);
748
- const connectorDelay = closerDist * animT;
749
- const retractDelay = (maxHoveredDistRef.current - closerDist) * animT;
750
- const drawRightToLeft = targetDist < depDist;
751
- const start = gid(dep);
752
- const end = gid(first);
753
- if (!start || !end) return null;
754
- const x1 = ox(start) + start.offsetWidth - 2;
755
- const y1 = nodeConnectorY(start);
756
- const x2 = ox(end) + 2;
757
- const y2 = nodeConnectorY(end);
758
- const selected = selectedEdges.some((edge) => edge.from === dep && edge.to === first);
759
- const dy = y2 - y1;
760
- const ady = Math.abs(dy);
761
- let pathD;
762
- let pathLen;
763
- let pathDReversed;
764
- const r = arcR;
765
- if (ady === 0) {
766
- pathD = `M ${x1} ${y1} L ${x2} ${y2}`;
767
- pathDReversed = `M ${x2} ${y2} L ${x1} ${y1}`;
768
- pathLen = x2 - x1;
769
- } else {
770
- const s = dy > 0 ? 1 : -1;
771
- const ax2end = x2 - (colGap / 2 - r);
772
- const ax = ax2end - 2 * r;
773
- const sw1 = dy > 0 ? 1 : 0;
774
- const sw2 = dy > 0 ? 0 : 1;
775
- pathD = [
776
- `M ${x1} ${y1}`,
777
- `L ${ax} ${y1}`,
778
- `A ${r} ${r} 0 0 ${sw1} ${ax + r} ${y1 + s * r}`,
779
- `L ${ax + r} ${y2 - s * r}`,
780
- `A ${r} ${r} 0 0 ${sw2} ${ax2end} ${y2}`,
781
- `L ${x2} ${y2}`
782
- ].join(" ");
783
- pathDReversed = [
784
- `M ${x2} ${y2}`,
785
- `L ${ax2end} ${y2}`,
786
- `A ${r} ${r} 0 0 ${sw1} ${ax + r} ${y2 - s * r}`,
787
- `L ${ax + r} ${y1 + s * r}`,
788
- `A ${r} ${r} 0 0 ${sw2} ${ax} ${y1}`,
789
- `L ${x1} ${y1}`
790
- ].join(" ");
791
- const arcLen = Math.PI * r / 2;
792
- const vSeg = Math.max(0, ady - 2 * r);
793
- pathLen = ax - x1 + arcLen + vSeg + arcLen + (x2 - ax2end);
794
- }
795
- if (editable) hits.push(/* @__PURE__ */ jsxs(React.Fragment, { children: [selected && /* @__PURE__ */ jsx("path", {
796
- d: pathD,
797
- stroke: "var(--theme-element)",
798
- strokeWidth: "10",
799
- fill: "transparent"
800
- }), /* @__PURE__ */ jsx(EdgeHitPath, {
801
- d: pathD,
802
- edge: {
803
- from: dep,
804
- to: first
805
- }
806
- })] }, `${dep}→${first}`));
807
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
808
- /* @__PURE__ */ jsxs("svg", {
809
- "aria-hidden": "true",
810
- overflow: "visible",
811
- className: "absolute pointer-events-none",
812
- style: { zIndex: (isHighlighted || activeDists?.has(dep) && activeDists?.has(first) ? 30 : 10) + zBase },
813
- children: [
814
- /* @__PURE__ */ jsx("rect", {
815
- x: x1 - 16,
816
- y: y1 - 16,
817
- width: 32,
818
- height: 32,
819
- fill: "var(--theme-panel-bg)"
820
- }),
821
- /* @__PURE__ */ jsx("rect", {
822
- x: x2 - 16,
823
- y: y2 - 16,
824
- width: 32,
825
- height: 32,
826
- fill: "var(--theme-panel-bg)"
827
- }),
828
- /* @__PURE__ */ jsx("circle", {
829
- cx: x1,
830
- cy: y1,
831
- r: "8",
832
- fill: isHighlighted ? HIGHLIGHT_COLOR : CONNECTOR_COLOR,
833
- style: {
834
- opacity: hoveredRelated && !isHighlighted ? .3 : 1,
835
- transition: isHighlighted ? `fill ${animT}s ${depDist * animT}s, opacity 0s` : `fill ${animT}s ${(maxHoveredDistRef.current - depDist) * animT}s, opacity 0s`
836
- }
837
- }),
838
- /* @__PURE__ */ jsx("circle", {
839
- cx: x2,
840
- cy: y2,
841
- r: "8",
842
- fill: isHighlighted ? HIGHLIGHT_COLOR : CONNECTOR_COLOR,
843
- style: {
844
- opacity: hoveredRelated && !isHighlighted ? .3 : 1,
845
- transition: isHighlighted ? `fill ${animT}s ${targetDist * animT}s, opacity 0s` : `fill ${animT}s ${(maxHoveredDistRef.current - targetDist) * animT}s, opacity 0s`
846
- }
847
- })
848
- ]
849
- }),
850
- /* @__PURE__ */ jsxs("svg", {
851
- "aria-hidden": "true",
852
- overflow: "visible",
853
- className: "absolute pointer-events-none",
854
- style: { zIndex: (isHighlighted ? 20 : -1) + zBase },
855
- children: [/* @__PURE__ */ jsx("path", {
856
- d: pathD,
857
- stroke: "var(--theme-panel-bg)",
858
- strokeWidth: "6",
859
- fill: "transparent"
860
- }), /* @__PURE__ */ jsx("path", {
861
- d: pathD,
862
- stroke: CONNECTOR_COLOR,
863
- strokeWidth: "6",
864
- fill: "transparent",
865
- style: {
866
- opacity: hoveredRelated && !isHighlighted ? .3 : 1,
867
- transition: `opacity 0s`
868
- }
869
- })]
870
- }),
871
- /* @__PURE__ */ jsxs("svg", {
872
- "aria-hidden": "true",
873
- overflow: "visible",
874
- className: "absolute pointer-events-none",
875
- style: { zIndex: 20 + zBase },
876
- children: [/* @__PURE__ */ jsx("path", {
877
- d: drawRightToLeft ? pathDReversed : pathD,
878
- fill: "transparent",
879
- style: {
880
- stroke: HIGHLIGHT_COLOR,
881
- strokeWidth: 6,
882
- strokeDasharray: `${pathLen} ${pathLen}`,
883
- strokeDashoffset: isHighlighted ? 0 : pathLen,
884
- opacity: isHighlighted ? 1 : 0,
885
- transition: isHighlighted ? `stroke-dashoffset ${animT}s ease ${connectorDelay}s, opacity 0s ease ${connectorDelay}s` : "stroke-dashoffset 0s, opacity 0s"
886
- }
887
- }), (() => {
888
- const connKey = `${dep}→${first}`;
889
- if (isHighlighted) retractPathLensRef.current.set(connKey, pathLen);
890
- const isRetracting = !isHighlighted && !!activeDists?.has(dep) && !!activeDists?.has(first);
891
- if (!isHighlighted && !isRetracting) return null;
892
- const frozenLen = retractPathLensRef.current.get(connKey) ?? pathLen;
893
- return /* @__PURE__ */ jsx("path", {
894
- d: drawRightToLeft ? pathDReversed : pathD,
895
- fill: "transparent",
896
- style: {
897
- stroke: HIGHLIGHT_COLOR,
898
- strokeWidth: 6,
899
- strokeDasharray: `${frozenLen} ${frozenLen}`,
900
- strokeDashoffset: isHighlighted ? 0 : frozenLen,
901
- opacity: isHighlighted ? 0 : 1,
902
- transition: isHighlighted ? "stroke-dashoffset 0s, opacity 0s" : `stroke-dashoffset ${animT}s ease ${retractDelay}s, opacity 0s`
903
- }
904
- });
905
- })()]
906
- }),
907
- editable && selected && /* @__PURE__ */ jsx(EdgeEnds, {
908
- edge: {
909
- from: dep,
910
- to: first
911
- },
912
- start: {
913
- x: x1,
914
- y: y1
915
- },
916
- end: {
917
- x: x2,
918
- y: y2
919
- },
920
- zIndex: 45 + zBase
921
- })
922
- ] }, dep);
923
- })] }, first);
924
- }) }, col[0]?.[0]));
925
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [connectors, editable && /* @__PURE__ */ jsx("svg", {
926
- role: "none",
927
- overflow: "visible",
928
- className: "absolute pointer-events-none",
929
- width: wrapper?.offsetWidth,
930
- height: wrapper?.offsetHeight,
931
- style: { zIndex: 1 + zBase },
932
- children: hits
933
- })] });
934
- })(),
935
- hasLiveNestedGraph ? buildBoxLayer() : memoizedBoxLayer,
936
- editable && /* @__PURE__ */ jsx(GraphEditorCanvas, {})
937
- ]
505
+ children: [/* @__PURE__ */ jsxs(LineageContext, {
506
+ value: lineage,
507
+ children: [
508
+ /* @__PURE__ */ jsx(DimRules, { depth }),
509
+ /* @__PURE__ */ jsx(ConnectorLayers, {
510
+ edges,
511
+ zBase
512
+ }),
513
+ editable && /* @__PURE__ */ jsx(EditorEdges, {
514
+ edges,
515
+ cols: dependencyCols,
516
+ boxes,
517
+ selected: selectedEdges,
518
+ zBase,
519
+ size: wrapper ? {
520
+ width: wrapper.offsetWidth,
521
+ height: wrapper.offsetHeight
522
+ } : void 0
523
+ }),
524
+ nodes
525
+ ]
526
+ }), editable && /* @__PURE__ */ jsx(GraphEditorCanvas, {})]
938
527
  });
939
- }
528
+ });
940
529
  var NestedSubgraph = Subgraph;
530
+ function EditorEdges({ edges, cols, boxes, selected, zBase, size }) {
531
+ const wiredIn = new Set(edges.map((edge) => edge.to));
532
+ const wiredOut = new Set(edges.map((edge) => edge.from));
533
+ const chosen = new Set(selected.map((edge) => `${edge.from}→${edge.to}`));
534
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
535
+ cols.flatMap((col) => col.map((jobNames) => {
536
+ const [head] = jobNames;
537
+ const box = head === void 0 ? void 0 : boxes.get(head);
538
+ if (head === void 0 || !box) return null;
539
+ const y = box.y + 37;
540
+ return /* @__PURE__ */ jsx(NodePorts, {
541
+ jobs: jobNames,
542
+ left: {
543
+ x: box.x + 2,
544
+ y
545
+ },
546
+ right: {
547
+ x: box.x + box.width - 2,
548
+ y
549
+ },
550
+ drawLeft: !wiredIn.has(head),
551
+ drawRight: !wiredOut.has(head),
552
+ zIndex: 40 + zBase
553
+ }, head);
554
+ })),
555
+ edges.map(({ from, to, start, end }) => chosen.has(`${from}→${to}`) && /* @__PURE__ */ jsx(EdgeEnds, {
556
+ edge: {
557
+ from,
558
+ to
559
+ },
560
+ start,
561
+ end,
562
+ zIndex: 45 + zBase
563
+ }, `${from}→${to}`)),
564
+ /* @__PURE__ */ jsx("svg", {
565
+ role: "none",
566
+ overflow: "visible",
567
+ className: "absolute pointer-events-none",
568
+ width: size?.width,
569
+ height: size?.height,
570
+ style: { zIndex: 1 + zBase },
571
+ children: edges.map(({ from, to, path }) => /* @__PURE__ */ jsxs(Fragment, { children: [chosen.has(`${from}→${to}`) && /* @__PURE__ */ jsx("path", {
572
+ d: path.d,
573
+ stroke: "var(--theme-element)",
574
+ strokeWidth: "10",
575
+ fill: "transparent"
576
+ }), /* @__PURE__ */ jsx(EdgeHitPath, {
577
+ d: path.d,
578
+ edge: {
579
+ from,
580
+ to
581
+ }
582
+ })] }, `${from}→${to}`))
583
+ })
584
+ ] });
585
+ }
941
586
  function SidebarJobs({ jobs, pathPrefix, preview, stepsOpen, toggleSteps, expandedSubworkflows, toggleSubworkflow, matrixOpen, toggleMatrix, openInNewGraph, setSublogOpen }) {
942
587
  const engine = useWorkflowEngine();
943
588
  const displayJobs = useMemo(() => displayJobsOf(engine, jobs, engine.matrixGroups(jobs)), [engine, jobs]);
@@ -1023,11 +668,37 @@ function walkJobPath(root, path) {
1023
668
  }
1024
669
  return isJobRecord(cursor) ? cursor : {};
1025
670
  }
671
+ function RunTime({ start, end, finished }) {
672
+ const [now, setNow] = useState(() => DateTime.now());
673
+ const ticking = !end && !finished;
674
+ useEffect(() => {
675
+ if (!ticking) return;
676
+ const timer = setInterval(() => setNow(DateTime.now()), 1e3);
677
+ return () => clearInterval(timer);
678
+ }, [ticking]);
679
+ return toAbsHumanDuration(start, end ?? now);
680
+ }
681
+ function graphBounds(content) {
682
+ const nodes = outerNodes(content);
683
+ if (nodes.length === 0) return null;
684
+ const bounds = {
685
+ minX: Number.POSITIVE_INFINITY,
686
+ minY: Number.POSITIVE_INFINITY,
687
+ maxX: 0,
688
+ maxY: 0
689
+ };
690
+ for (const node of nodes) {
691
+ const { x, y } = offsetWithin(node, content);
692
+ bounds.minX = Math.min(bounds.minX, x);
693
+ bounds.minY = Math.min(bounds.minY, y);
694
+ bounds.maxX = Math.max(bounds.maxX, x + node.offsetWidth);
695
+ bounds.maxY = Math.max(bounds.maxY, y + node.offsetHeight);
696
+ }
697
+ return bounds;
698
+ }
1026
699
  function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight }) {
1027
700
  const engine = useWorkflowEngine();
1028
701
  const animT = .125;
1029
- const minScale = .2;
1030
- const maxScale = .6;
1031
702
  const rootJobs = isJobRecord(run.executedJobs) ? run.executedJobs : {};
1032
703
  const runNumber = run.number ?? 0;
1033
704
  const runSlug = run.slug ?? "";
@@ -1039,8 +710,8 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1039
710
  const containerRef = useRef(null);
1040
711
  const graphContentRef = useRef(null);
1041
712
  const transformRef = useRef(null);
1042
- const [, forceRerender] = useState(0);
1043
- const invalidateConnectors = useCallback(() => forceRerender((c) => c + 1), []);
713
+ const [clock] = useState(() => new ConnectorClock());
714
+ const invalidateConnectors = clock.bump;
1044
715
  const [windowSize, setWindowSize] = useState(() => ({
1045
716
  w: typeof window !== "undefined" ? window.innerWidth : 800,
1046
717
  h: typeof window !== "undefined" ? window.innerHeight : 600
@@ -1115,13 +786,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1115
786
  const [executedJobsPathIdx, setExecutedJobsPathIdx] = useState(0);
1116
787
  const [expandedJobs, setExpandedJobs] = useState(/* @__PURE__ */ new Set());
1117
788
  const [matrixOpen, setMatrixOpen] = useState(/* @__PURE__ */ new Set());
1118
- const isRunFinished = !!run.completedAt || run.status === "completed" || run.status === "error" || run.status === "canceled";
1119
- const [now, setNow] = useState(() => DateTime.now());
1120
- useEffect(() => {
1121
- if (isRunFinished) return;
1122
- const timer = setInterval(() => setNow(DateTime.now()), 1e3);
1123
- return () => clearInterval(timer);
1124
- }, [isRunFinished]);
789
+ const [shownAt] = useState(() => DateTime.now());
1125
790
  const currentPath = executedJobsPath[executedJobsPathIdx] ?? [];
1126
791
  const jobs = walkJobPath(rootJobs, currentPath);
1127
792
  const root = currentPath.length === 0;
@@ -1192,18 +857,19 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1192
857
  const sublogIsSubworkflow = !!sublogStepData?.subworkflow;
1193
858
  const sublogFileStart = sublogOpen.lastIndexOf("/") + 1;
1194
859
  const sublogShowsLog = sublogOpen.slice(sublogFileStart) === "logs.out";
1195
- const matrixGroups = useMemo(() => engine.matrixGroups(jobs), [engine, jobs]);
1196
- const { dependencyCols } = useMemo(() => computeGraphLayout(engine, jobs, matrixGroups), [
860
+ const rootGraph = useMemo(() => computeGraphLayout(engine, jobs, engine.matrixGroups(jobs), placed), [
1197
861
  engine,
1198
862
  jobs,
1199
- matrixGroups
863
+ placed
1200
864
  ]);
865
+ const { dependencyCols } = rootGraph;
1201
866
  const held = useRef(null);
1202
867
  const viewed = useRef(false);
868
+ const rowOf = useCallback((job) => [...containerRef.current?.querySelectorAll("[data-dag-job]") ?? []].find((el) => el.dataset["dagJob"] === job) ?? null, []);
1203
869
  const nodeOf = useCallback((job) => {
1204
- const row = [...containerRef.current?.querySelectorAll("[data-dag-job]") ?? []].find((el) => el.dataset["dagJob"] === job);
1205
- return row?.closest("[id^=\"node_\"]") ?? row ?? null;
1206
- }, []);
870
+ const row = rowOf(job);
871
+ return row?.closest("[id^=\"node_\"]") ?? row;
872
+ }, [rowOf]);
1207
873
  const hold = useCallback((job) => {
1208
874
  const content = graphContentRef.current;
1209
875
  const transform = transformRef.current;
@@ -1222,65 +888,38 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1222
888
  const container = containerRef.current;
1223
889
  const content = graphContentRef.current;
1224
890
  const transform = transformRef.current;
1225
- if (!container || !content || !transform) return;
1226
- const nodes = outerNodes(content);
1227
- if (nodes.length === 0) return;
1228
- let minX = Number.POSITIVE_INFINITY;
1229
- let minY = Number.POSITIVE_INFINITY;
1230
- let maxX = 0;
1231
- let maxY = 0;
1232
- for (const node of nodes) {
1233
- const el = node;
1234
- const pos = offsetWithin(el, content);
1235
- minX = Math.min(minX, pos.x);
1236
- minY = Math.min(minY, pos.y);
1237
- maxX = Math.max(maxX, pos.x + el.offsetWidth);
1238
- maxY = Math.max(maxY, pos.y + el.offsetHeight);
1239
- }
1240
- const graphW = maxX - minX;
1241
- const graphH = maxY - minY;
1242
- const containerW = container.clientWidth;
1243
- const containerH = container.clientHeight;
1244
- const margin = Math.min(containerW, containerH) * .05;
891
+ const bounds = content ? graphBounds(content) : null;
892
+ if (!container || !content || !transform || !bounds) return;
893
+ const panel = {
894
+ width: container.clientWidth,
895
+ height: container.clientHeight
896
+ };
1245
897
  const pending = held.current;
1246
898
  held.current = null;
1247
899
  const node = pending && performance.now() - pending.at < HOLD_MS ? nodeOf(pending.job) : null;
1248
900
  const viewing = editor && viewed.current && animationMs === 0;
1249
901
  if (pending && node || viewing) {
1250
- const { positionX, positionY, scale: current } = transform.instance.state;
902
+ const { positionX, positionY, scale } = transform.instance.state;
1251
903
  const pos = pending && node ? offsetWithin(node, content) : null;
1252
- const x = pending && pos ? pending.x - pos.x * current : positionX;
1253
- const y = pending && pos ? pending.y - pos.y * current : positionY;
1254
- transform.setTransform(x, y, current, 0);
1255
- const dx = intoView(x + minX * current, x + maxX * current, margin, containerW - margin);
1256
- const dy = intoView(y + minY * current, y + maxY * current, margin, containerH - margin);
1257
- requestAnimationFrame(() => {
1258
- if (dx !== 0 || dy !== 0) transform.setTransform(x + dx, y + dy, current, FIT_ANIMATION_MS, "easeOut");
1259
- invalidateConnectors();
1260
- });
904
+ const x = pending && pos ? pending.x - pos.x * scale : positionX;
905
+ const y = pending && pos ? pending.y - pos.y * scale : positionY;
906
+ transform.setTransform(x, y, scale, 0);
907
+ const margin = fitMargin(panel);
908
+ const dx = intoView(x + bounds.minX * scale, x + bounds.maxX * scale, margin, panel.width - margin);
909
+ const dy = intoView(y + bounds.minY * scale, y + bounds.maxY * scale, margin, panel.height - margin);
910
+ if (dx !== 0 || dy !== 0) requestAnimationFrame(() => transform.setTransform(x + dx, y + dy, scale, FIT_ANIMATION_MS, "easeOut"));
1261
911
  return;
1262
912
  }
1263
- if (graphW === 0 || graphH === 0) return;
1264
- const scaleW = (containerW - margin * 2) / graphW;
1265
- const scaleH = (containerH - margin * 2) / graphH;
1266
- const scale = Math.min(Math.max(Math.min(scaleW, scaleH), minScale), maxScale);
1267
- const offsetX = margin - minX * scale;
1268
- const offsetY = margin - minY * scale;
913
+ const view = fitView(bounds, panel);
914
+ if (!view) return;
1269
915
  if (animationMs > 0) {
1270
- transform.setTransform(offsetX, offsetY, scale, animationMs, "easeOut");
916
+ transform.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
1271
917
  return;
1272
918
  }
1273
919
  const wrapperEl = container.querySelector(".react-transform-component");
1274
- if (wrapperEl) wrapperEl.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
1275
- requestAnimationFrame(() => {
1276
- transform.setTransform(offsetX, offsetY, scale, 0);
1277
- invalidateConnectors();
1278
- });
1279
- }, [
1280
- invalidateConnectors,
1281
- nodeOf,
1282
- editor
1283
- ]);
920
+ if (wrapperEl) wrapperEl.style.transform = `translate(${view.x}px, ${view.y}px) scale(${view.scale})`;
921
+ requestAnimationFrame(() => transform.setTransform(view.x, view.y, view.scale, 0));
922
+ }, [nodeOf, editor]);
1284
923
  const fitWhenSettled = useCallback(async () => {
1285
924
  const container = containerRef.current;
1286
925
  const content = graphContentRef.current;
@@ -1323,7 +962,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1323
962
  useEffect(() => {
1324
963
  if (!registerShow || !editorApi) return;
1325
964
  return registerShow(({ job, step }) => {
1326
- const row = [...containerRef.current?.querySelectorAll("[data-dag-job]") ?? []].find((el) => el.dataset["dagJob"] === job);
965
+ const row = job === void 0 ? null : rowOf(job);
1327
966
  if (job === void 0 || !row) return false;
1328
967
  if (step !== void 0) setStepsOpen((prev) => new Set(prev).add(job));
1329
968
  editorApi.focus(step === void 0 ? { job } : {
@@ -1338,28 +977,54 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1338
977
  }, [
1339
978
  registerShow,
1340
979
  editorApi,
1341
- centerOn
980
+ centerOn,
981
+ rowOf
1342
982
  ]);
1343
- const [zoomedAll, setZoomedAll] = useState("in");
1344
- const onTransform = useCallback((_, { scale }) => {
1345
- setZoomedAll(scale >= .599 ? "in" : scale <= .201 ? "out" : null);
1346
- invalidateConnectors();
1347
- }, [invalidateConnectors]);
983
+ const [zoomedAll, setZoomedAll] = useState(() => zoomLimit(FIT_MAX_SCALE));
984
+ const onTransform = useCallback((_, { scale }) => setZoomedAll(zoomLimit(scale)), []);
1348
985
  const moved = useCallback(() => {
1349
986
  viewed.current = true;
1350
- invalidateConnectors();
1351
- }, [invalidateConnectors]);
1352
- const zoomBy = useCallback((step) => {
1353
- const container = containerRef.current;
1354
- const transform = transformRef.current;
1355
- if (!container || !transform) return;
987
+ }, []);
988
+ const zoomTo = useCallback((view, animationMs) => {
1356
989
  viewed.current = true;
1357
- const { positionX, positionY, scale } = transform.instance.state;
1358
- const next = Math.min(maxScale, Math.max(minScale, scale * Math.exp(step)));
1359
- const x = container.clientWidth / 2;
1360
- const y = container.clientHeight / 2;
1361
- transform.setTransform(x - (x - positionX) / scale * next, y - (y - positionY) / scale * next, next, FIT_ANIMATION_MS, "easeOut");
990
+ transformRef.current?.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
1362
991
  }, []);
992
+ const currentView = () => {
993
+ const state = transformRef.current?.instance.state;
994
+ return state ? {
995
+ x: state.positionX,
996
+ y: state.positionY,
997
+ scale: state.scale
998
+ } : null;
999
+ };
1000
+ const zoomBy = (step) => {
1001
+ const container = containerRef.current;
1002
+ const view = currentView();
1003
+ if (container && view) {
1004
+ const middle = {
1005
+ x: container.clientWidth / 2,
1006
+ y: container.clientHeight / 2
1007
+ };
1008
+ zoomTo(zoomAround(view, middle, Math.exp(step)), FIT_ANIMATION_MS);
1009
+ }
1010
+ };
1011
+ useEffect(() => {
1012
+ const wrapper = transformRef.current?.instance.wrapperComponent;
1013
+ if (!wrapper) return;
1014
+ const onWheel = (event) => {
1015
+ const view = currentView();
1016
+ if (!view) return;
1017
+ event.preventDefault();
1018
+ const rect = wrapper.getBoundingClientRect();
1019
+ const at = {
1020
+ x: event.clientX - rect.left,
1021
+ y: event.clientY - rect.top
1022
+ };
1023
+ zoomTo(zoomAround(view, at, wheelFactor(event)), 0);
1024
+ };
1025
+ wrapper.addEventListener("wheel", onWheel, { passive: false });
1026
+ return () => wrapper.removeEventListener("wheel", onWheel);
1027
+ }, [containerEl, zoomTo]);
1363
1028
  useEffect(() => {
1364
1029
  if (preview) return;
1365
1030
  const raf = requestAnimationFrame(() => fitGraph());
@@ -1476,9 +1141,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1476
1141
  removeBorder
1477
1142
  })
1478
1143
  }) : void 0;
1479
- const startDt = run.createdAt ? DateTime.fromISO(run.createdAt) : now;
1480
- const endDt = run.completedAt ? DateTime.fromISO(run.completedAt) : now;
1481
- const runtime = toAbsHumanDuration(startDt, endDt);
1144
+ const startDt = run.createdAt ? DateTime.fromISO(run.createdAt) : shownAt;
1482
1145
  const additionalBottomLeftBarComponents = sublogIsStep ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1483
1146
  /* @__PURE__ */ jsxs("button", {
1484
1147
  type: "button",
@@ -1586,7 +1249,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1586
1249
  children: dagStrings.runtime
1587
1250
  }), /* @__PURE__ */ jsx("div", {
1588
1251
  className: "text-sm font-semibold flex items-center gap-x-1",
1589
- children: runtime
1252
+ children: /* @__PURE__ */ jsx(RunTime, {
1253
+ start: startDt,
1254
+ end: run.completedAt ? DateTime.fromISO(run.completedAt) : void 0,
1255
+ finished: run.status === "completed" || run.status === "error" || run.status === "canceled"
1256
+ })
1590
1257
  })]
1591
1258
  }),
1592
1259
  /* @__PURE__ */ jsxs("div", {
@@ -1631,9 +1298,10 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1631
1298
  },
1632
1299
  children: [/* @__PURE__ */ jsx(TransformWrapper, {
1633
1300
  ref: transformRef,
1634
- minScale,
1635
- maxScale,
1636
- initialScale: maxScale,
1301
+ minScale: MIN_SCALE,
1302
+ maxScale: 2,
1303
+ initialScale: FIT_MAX_SCALE,
1304
+ wheel: { disabled: true },
1637
1305
  ...editor ? { panning: { excluded: [EDITOR_HANDLE_CLASS] } } : {},
1638
1306
  doubleClick: { disabled: true },
1639
1307
  limitToBounds: false,
@@ -1643,8 +1311,6 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1643
1311
  onPanning: moved,
1644
1312
  onPinch: moved,
1645
1313
  onTransform,
1646
- onWheel: moved,
1647
- onZoom: invalidateConnectors,
1648
1314
  children: () => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1649
1315
  className: "absolute z-10 top-2 right-2 flex max-w-[calc(100%-1rem)] flex-col items-end gap-y-1",
1650
1316
  children: /* @__PURE__ */ jsx(GraphViewControls, {
@@ -1666,30 +1332,34 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1666
1332
  onZoom: (direction) => zoomBy(direction * ZOOM_STEP),
1667
1333
  zoomedAll
1668
1334
  })
1669
- }), /* @__PURE__ */ jsx(TransformComponent, { children: /* @__PURE__ */ jsx("div", {
1670
- ref: graphContentRef,
1671
- className: "flex relative mr-[8000px]",
1672
- style: { minHeight: 4e3 },
1673
- children: /* @__PURE__ */ jsx(Subgraph, {
1674
- jobs,
1675
- pathPrefix: pathAppend,
1676
- depth: 0,
1677
- preview,
1678
- animT,
1679
- expandedSubworkflows,
1680
- toggleSubworkflow,
1681
- openInNewGraph,
1682
- setSublogOpen,
1683
- invalidateConnectors,
1684
- requestFit: fitWhenSettled,
1685
- stepsOpen,
1686
- toggleSteps,
1687
- matrixOpen,
1688
- toggleMatrix,
1689
- onReady: fitGraph,
1690
- layout: placed,
1691
- editable: !!editorApi && currentPath.length === 0
1692
- }, `sg-${executedJobsPathIdx}-${pathAppend}`)
1335
+ }), /* @__PURE__ */ jsx(TransformComponent, { children: /* @__PURE__ */ jsx(ConnectorClockContext, {
1336
+ value: clock,
1337
+ children: /* @__PURE__ */ jsx("div", {
1338
+ ref: graphContentRef,
1339
+ className: "flex relative mr-[8000px]",
1340
+ style: { minHeight: 4e3 },
1341
+ children: /* @__PURE__ */ jsx(Subgraph, {
1342
+ jobs,
1343
+ pathPrefix: pathAppend,
1344
+ depth: 0,
1345
+ preview,
1346
+ animT,
1347
+ expandedSubworkflows,
1348
+ toggleSubworkflow,
1349
+ openInNewGraph,
1350
+ setSublogOpen,
1351
+ invalidateConnectors,
1352
+ requestFit: fitWhenSettled,
1353
+ stepsOpen,
1354
+ toggleSteps,
1355
+ matrixOpen,
1356
+ toggleMatrix,
1357
+ onReady: fitGraph,
1358
+ layout: placed,
1359
+ graph: rootGraph,
1360
+ editable: !!editorApi && currentPath.length === 0
1361
+ }, `sg-${executedJobsPathIdx}-${pathAppend}`)
1362
+ })
1693
1363
  }) })] })
1694
1364
  }), editorOverlays]
1695
1365
  })
@@ -1745,26 +1415,6 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1745
1415
  children: graph
1746
1416
  });
1747
1417
  }
1748
- var isSlot = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
1749
- /** Each job's place on the graph from its `position`; none when no job has one. */
1750
- function positionsOf(jobs) {
1751
- const layout = Object.create(null);
1752
- let any = false;
1753
- for (const [name, job] of Object.entries(jobs)) {
1754
- const { position, _matrix } = isJobRecord(job) ? job : {};
1755
- const { column, row } = isJobRecord(position) ? position : {};
1756
- const original = isJobRecord(_matrix) ? _matrix["originaljob"] : void 0;
1757
- const key = typeof original === "string" && original ? original : name;
1758
- if (isSlot(column) && isSlot(row) && !(key in layout)) {
1759
- layout[key] = {
1760
- column,
1761
- row
1762
- };
1763
- any = true;
1764
- }
1765
- }
1766
- return any ? layout : void 0;
1767
- }
1768
1418
  /** Jobs as a run evaluates them before it starts: each `strategy`, and a `needs` expression, from the inputs' defaults. */
1769
1419
  function withRunExpressions(engine, jobs, workflowInputs) {
1770
1420
  let values;
@@ -1871,4 +1521,4 @@ function DependencyGraphPreview(inputs) {
1871
1521
  });
1872
1522
  }
1873
1523
  //#endregion
1874
- export { DependencyGraphPreview, addCleanupSteps, computeGraphLayout, DependencyGraph as default, intoView };
1524
+ export { DependencyGraphPreview, addCleanupSteps, DependencyGraph as default };