@parallelworks/ui 0.40.0 → 0.41.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 +6 -0
- package/dist/ai/adapter/memoryStore.js +7 -0
- package/dist/ai/adapter/mock/mockAdapter.js +52 -17
- package/dist/ai/adapter/openai/createOpenAIStream.js +1 -1
- package/dist/ai/adapter/openai/index.d.ts +1 -1
- package/dist/ai/adapter/openai/sse.d.ts +8 -2
- package/dist/ai/adapter/openai/sse.js +40 -4
- package/dist/ai/adapter/openai/wire.d.ts +6 -0
- package/dist/ai/ai.css +4 -2
- package/dist/ai/core/ChatProvider.js +3 -3
- package/dist/ai/core/parts.d.ts +5 -0
- package/dist/ai/core/parts.js +28 -1
- package/dist/ai/ui/Markdown.js +4 -1
- package/dist/graph/Connectors.d.ts +11 -0
- package/dist/graph/Connectors.js +158 -0
- package/dist/graph/DependencyGraph.d.ts +2 -16
- package/dist/graph/DependencyGraph.js +375 -725
- package/dist/graph/GraphNode.d.ts +35 -0
- package/dist/graph/GraphNode.js +70 -0
- package/dist/graph/MatrixGroup.d.ts +4 -8
- package/dist/graph/MatrixGroup.js +27 -40
- package/dist/graph/connectorGeometry.d.ts +44 -0
- package/dist/graph/connectorGeometry.js +103 -0
- package/dist/graph/graphLayout.d.ts +24 -0
- package/dist/graph/graphLayout.js +205 -0
- package/dist/graph/lineage.d.ts +51 -0
- package/dist/graph/lineage.js +94 -0
- package/dist/graph/store.d.ts +10 -0
- package/dist/graph/store.js +25 -0
- package/dist/graph/viewport.d.ts +40 -0
- package/dist/graph/viewport.js +56 -0
- package/dist/styles.css +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type JobHandlers } from './JobSummary.js';
|
|
3
|
+
import type { WorkflowJob } from './types.js';
|
|
4
|
+
export declare const dimClasses: (depth: number) => {
|
|
5
|
+
level: string;
|
|
6
|
+
node: string;
|
|
7
|
+
edge: string;
|
|
8
|
+
};
|
|
9
|
+
/** The rules dimming a level at `depth`, hoisted into the page once for every level that deep. */
|
|
10
|
+
export declare function DimRules({ depth }: {
|
|
11
|
+
depth: number;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Dims what of a graph level isn't in the hovered job's lineage, while one is hovered. */
|
|
14
|
+
export declare function setDimmed(wrapper: HTMLElement, depth: number, hovering: boolean): void;
|
|
15
|
+
interface NodeShell {
|
|
16
|
+
id: string;
|
|
17
|
+
/** Empty rows of its column above this node. */
|
|
18
|
+
rowsAbove: number;
|
|
19
|
+
zBase: number;
|
|
20
|
+
}
|
|
21
|
+
/** One node of the graph: its jobs' box, raised above connectors while its lineage is shown. */
|
|
22
|
+
export declare function GraphNode({ id, job, rowsAbove, zBase, children, }: NodeShell & {
|
|
23
|
+
/** The job hovering the node shows the lineage of. */
|
|
24
|
+
job: string;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/** A node of jobs that share their needs and dependents. */
|
|
28
|
+
export declare const JobNode: import("react").MemoExoticComponent<({ jobNames, jobs, handlers, preview, editable, ...shell }: NodeShell & {
|
|
29
|
+
jobNames: string[];
|
|
30
|
+
jobs: Record<string, WorkflowJob>;
|
|
31
|
+
handlers: JobHandlers;
|
|
32
|
+
preview: boolean;
|
|
33
|
+
editable: boolean;
|
|
34
|
+
}) => import("react").JSX.Element>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BoxGrip } from "./editorRows.js";
|
|
2
|
+
import { emptyRowsStyle } from "./gridSpacing.js";
|
|
3
|
+
import { Joblist } from "./JobSummary.js";
|
|
4
|
+
import { useLineage, useLineageStore } from "./lineage.js";
|
|
5
|
+
import { memo } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/graph/GraphNode.tsx
|
|
8
|
+
var dimClasses = (depth) => ({
|
|
9
|
+
level: `dag-dim-${depth}`,
|
|
10
|
+
node: `dag-node-${depth}`,
|
|
11
|
+
edge: `dag-edge-${depth}`
|
|
12
|
+
});
|
|
13
|
+
/** The rules dimming a level at `depth`, hoisted into the page once for every level that deep. */
|
|
14
|
+
function DimRules({ depth }) {
|
|
15
|
+
const { level, node, edge } = dimClasses(depth);
|
|
16
|
+
return /* @__PURE__ */ jsx("style", {
|
|
17
|
+
href: `dependency-graph-dim-${depth}`,
|
|
18
|
+
precedence: "default",
|
|
19
|
+
children: `.${level} .${node}{opacity:.5}.${level} .${edge}{opacity:.3}`
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Dims what of a graph level isn't in the hovered job's lineage, while one is hovered. */
|
|
23
|
+
function setDimmed(wrapper, depth, hovering) {
|
|
24
|
+
wrapper.classList.toggle(dimClasses(depth).level, hovering);
|
|
25
|
+
}
|
|
26
|
+
/** One node of the graph: its jobs' box, raised above connectors while its lineage is shown. */
|
|
27
|
+
function GraphNode({ id, job, rowsAbove, zBase, children }) {
|
|
28
|
+
const lineage = useLineageStore();
|
|
29
|
+
const highlight = useLineage((l) => l.node(job), 0);
|
|
30
|
+
const enter = () => lineage?.hover(job);
|
|
31
|
+
const leave = () => lineage?.hover(null);
|
|
32
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
33
|
+
id,
|
|
34
|
+
role: "none",
|
|
35
|
+
className: "relative m-24",
|
|
36
|
+
style: {
|
|
37
|
+
zIndex: (highlight > 0 ? 25 : 1) + zBase,
|
|
38
|
+
...emptyRowsStyle(rowsAbove)
|
|
39
|
+
},
|
|
40
|
+
onMouseEnter: enter,
|
|
41
|
+
onMouseLeave: leave,
|
|
42
|
+
onFocus: enter,
|
|
43
|
+
onBlur: leave,
|
|
44
|
+
children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-2xl bg-(--theme-panel-bg)" }), /* @__PURE__ */ jsx("div", {
|
|
45
|
+
className: `${dimClasses(lineage?.depth ?? 0).node} relative border-solid shadow py-4 px-8 rounded-xl border-4 whitespace-nowrap bg-(--theme-panel-bg) text-2xl`,
|
|
46
|
+
style: {
|
|
47
|
+
...highlight === 2 && { opacity: 1 },
|
|
48
|
+
...lineage?.fade && { transition: `opacity ${lineage.animT}s` }
|
|
49
|
+
},
|
|
50
|
+
children
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** A node of jobs that share their needs and dependents. */
|
|
55
|
+
var JobNode = memo(function JobNode({ jobNames, jobs, handlers, preview, editable, ...shell }) {
|
|
56
|
+
return /* @__PURE__ */ jsxs(GraphNode, {
|
|
57
|
+
...shell,
|
|
58
|
+
job: jobNames[0] ?? "",
|
|
59
|
+
children: [editable && jobNames.length > 1 && /* @__PURE__ */ jsx(BoxGrip, { jobs: jobNames }), /* @__PURE__ */ jsx(Joblist, {
|
|
60
|
+
jobs,
|
|
61
|
+
jobNames,
|
|
62
|
+
...handlers,
|
|
63
|
+
growWidth: true,
|
|
64
|
+
preview,
|
|
65
|
+
editable
|
|
66
|
+
})]
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
export { DimRules, GraphNode, JobNode, dimClasses, setDimmed };
|
|
@@ -12,22 +12,18 @@ export declare function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded
|
|
|
12
12
|
preview: boolean;
|
|
13
13
|
}): import("react").JSX.Element;
|
|
14
14
|
/** Matrix group node in the DAG visualization. */
|
|
15
|
-
export declare
|
|
15
|
+
export declare const MatrixGroupNode: import("react").MemoExoticComponent<({ matrixGroup, jobNames, idPrefix, isExpanded, toggleMatrix, jobs, zBase, handlers, preview, editable, rowsAbove, }: {
|
|
16
16
|
matrixGroup: MatrixGroup;
|
|
17
17
|
jobNames: string[];
|
|
18
18
|
idPrefix: string;
|
|
19
19
|
isExpanded: boolean;
|
|
20
|
-
|
|
20
|
+
/** Opens or closes the matrix by its key, its path prefix and job. */
|
|
21
|
+
toggleMatrix: (key: string) => void;
|
|
21
22
|
jobs: Record<string, WorkflowJob>;
|
|
22
|
-
activeDists: Map<string, number> | null;
|
|
23
|
-
onMouseEnter: () => void;
|
|
24
|
-
onMouseLeave: () => void;
|
|
25
|
-
hoveredRelated: Set<string> | null;
|
|
26
|
-
animT: number;
|
|
27
23
|
zBase: number;
|
|
28
24
|
handlers: JobHandlers;
|
|
29
25
|
preview: boolean;
|
|
30
26
|
editable?: boolean;
|
|
31
27
|
/** Empty rows of its column above this node. */
|
|
32
28
|
rowsAbove?: number;
|
|
33
|
-
})
|
|
29
|
+
}) => import("react").JSX.Element>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useStrings, useWorkflowEngine } from "../components/Provider.js";
|
|
2
2
|
import { Indicator } from "../components/Indicator.js";
|
|
3
3
|
import { EditableJobRow } from "./editorRows.js";
|
|
4
|
-
import { emptyRowsStyle } from "./gridSpacing.js";
|
|
5
4
|
import { Reveal } from "./Reveal.js";
|
|
6
5
|
import { Joblist } from "./JobSummary.js";
|
|
6
|
+
import { GraphNode } from "./GraphNode.js";
|
|
7
|
+
import { memo } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
//#region src/graph/MatrixGroup.tsx
|
|
9
10
|
/** Matrix group item in the summary sidebar. */
|
|
@@ -42,7 +43,7 @@ function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, job
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
/** Matrix group node in the DAG visualization. */
|
|
45
|
-
function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded,
|
|
46
|
+
var MatrixGroupNode = memo(function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded, toggleMatrix, jobs, zBase, handlers, preview, editable = false, rowsAbove = 0 }) {
|
|
46
47
|
const { dag: t } = useStrings();
|
|
47
48
|
const matrixName = matrixGroup.originaljob;
|
|
48
49
|
const first = jobNames[0] ?? "";
|
|
@@ -52,7 +53,7 @@ function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded, onToggle
|
|
|
52
53
|
type: "button",
|
|
53
54
|
"aria-expanded": isExpanded,
|
|
54
55
|
className: "cursor-pointer w-full text-left",
|
|
55
|
-
onClick:
|
|
56
|
+
onClick: () => toggleMatrix(idPrefix + matrixName),
|
|
56
57
|
children: [/* @__PURE__ */ jsx("div", {
|
|
57
58
|
className: "font-semibold text-md p-0.5",
|
|
58
59
|
children: t.matrixOf(matrixName.length > 20 ? `${matrixName.slice(0, 20)}...` : matrixName)
|
|
@@ -61,46 +62,32 @@ function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded, onToggle
|
|
|
61
62
|
children: [!preview && /* @__PURE__ */ jsx(Indicator, { status: aggStatus }), /* @__PURE__ */ jsx("span", { children: preview ? t.jobCount(matrixMembers.length) : statusLabel })]
|
|
62
63
|
})]
|
|
63
64
|
});
|
|
64
|
-
return /* @__PURE__ */ jsxs(
|
|
65
|
+
return /* @__PURE__ */ jsxs(GraphNode, {
|
|
65
66
|
id: `node_${idPrefix}${first}`,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
children: header
|
|
87
|
-
}) : header, /* @__PURE__ */ jsx(Reveal, {
|
|
88
|
-
open: isExpanded,
|
|
89
|
-
growWidth: true,
|
|
90
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
91
|
-
className: "mt-1 border-t border-(--theme-border) pt-1",
|
|
92
|
-
children: /* @__PURE__ */ jsx(Joblist, {
|
|
93
|
-
jobs,
|
|
94
|
-
jobNames: matrixMembers,
|
|
95
|
-
...handlers,
|
|
96
|
-
growWidth: true,
|
|
97
|
-
preview,
|
|
98
|
-
editableSteps: editable
|
|
99
|
-
})
|
|
67
|
+
job: first,
|
|
68
|
+
rowsAbove,
|
|
69
|
+
zBase,
|
|
70
|
+
children: [editable ? /* @__PURE__ */ jsx(EditableJobRow, {
|
|
71
|
+
job: matrixName,
|
|
72
|
+
label: t.matrixOf(matrixName),
|
|
73
|
+
badge: false,
|
|
74
|
+
children: header
|
|
75
|
+
}) : header, /* @__PURE__ */ jsx(Reveal, {
|
|
76
|
+
open: isExpanded,
|
|
77
|
+
growWidth: true,
|
|
78
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
79
|
+
className: "mt-1 border-t border-(--theme-border) pt-1",
|
|
80
|
+
children: /* @__PURE__ */ jsx(Joblist, {
|
|
81
|
+
jobs,
|
|
82
|
+
jobNames: matrixMembers,
|
|
83
|
+
...handlers,
|
|
84
|
+
growWidth: true,
|
|
85
|
+
preview,
|
|
86
|
+
editableSteps: editable
|
|
100
87
|
})
|
|
101
|
-
})
|
|
88
|
+
})
|
|
102
89
|
})]
|
|
103
90
|
});
|
|
104
|
-
}
|
|
91
|
+
});
|
|
105
92
|
//#endregion
|
|
106
93
|
export { MatrixGroupNode, MatrixGroupSummaryItem };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
/** A connector's path both ways, so its highlight can draw from either end. */
|
|
6
|
+
export interface ConnectorPath {
|
|
7
|
+
d: string;
|
|
8
|
+
reversed: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The corner radius every connector of a graph shares: a quarter of the least rise any connector
|
|
12
|
+
* makes, and no more than half the narrowest gap between columns, so bends never overlap.
|
|
13
|
+
*/
|
|
14
|
+
export declare function arcRadius(minRise: number, colGap: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* A connector from a node's right side to a later node's left: straight when level, otherwise
|
|
17
|
+
* across, a bend, down or up, a bend and across. The bends sit in the gap before `end`'s column, so
|
|
18
|
+
* a connector spanning several columns runs level until it nears its target.
|
|
19
|
+
*/
|
|
20
|
+
export declare function connectorPath(start: Point, end: Point, r: number, colGap: number): ConnectorPath;
|
|
21
|
+
/** A node's place in its graph level, as laid out. */
|
|
22
|
+
export interface Box {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
}
|
|
27
|
+
/** Whether two layouts put every node in the same place. */
|
|
28
|
+
export declare function sameBoxes(a: Map<string, Box>, b: Map<string, Box>): boolean;
|
|
29
|
+
/** A connector as drawn, from a need's node to the node that needs it. */
|
|
30
|
+
export interface DrawnEdge {
|
|
31
|
+
from: string;
|
|
32
|
+
to: string;
|
|
33
|
+
start: Point;
|
|
34
|
+
end: Point;
|
|
35
|
+
path: ConnectorPath;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Every connector of a graph level, from its nodes' boxes, keyed by each node's first job. A node's
|
|
39
|
+
* jobs share their needs, so its first job's needs are the node's; a need naming a node not laid out
|
|
40
|
+
* draws nothing. `connectorY` is how far below a node's top its connectors meet it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function routeConnectors(cols: string[][][],
|
|
43
|
+
/** Whether columns are left empty between those drawn, so gaps between them differ. */
|
|
44
|
+
spaced: boolean, deps: Record<string, string[]>, boxes: Map<string, Box>, connectorY: number): DrawnEdge[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//#region src/graph/connectorGeometry.ts
|
|
2
|
+
var MAX_ARC_R = 16;
|
|
3
|
+
/**
|
|
4
|
+
* The corner radius every connector of a graph shares: a quarter of the least rise any connector
|
|
5
|
+
* makes, and no more than half the narrowest gap between columns, so bends never overlap.
|
|
6
|
+
*/
|
|
7
|
+
function arcRadius(minRise, colGap) {
|
|
8
|
+
return Math.min(Number.isFinite(minRise) ? minRise / 4 : MAX_ARC_R, colGap / 2, MAX_ARC_R);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A connector from a node's right side to a later node's left: straight when level, otherwise
|
|
12
|
+
* across, a bend, down or up, a bend and across. The bends sit in the gap before `end`'s column, so
|
|
13
|
+
* a connector spanning several columns runs level until it nears its target.
|
|
14
|
+
*/
|
|
15
|
+
function connectorPath(start, end, r, colGap) {
|
|
16
|
+
const { x: x1, y: y1 } = start;
|
|
17
|
+
const { x: x2, y: y2 } = end;
|
|
18
|
+
const dy = y2 - y1;
|
|
19
|
+
if (dy === 0) return {
|
|
20
|
+
d: `M ${x1} ${y1} L ${x2} ${y2}`,
|
|
21
|
+
reversed: `M ${x2} ${y2} L ${x1} ${y1}`
|
|
22
|
+
};
|
|
23
|
+
const s = dy > 0 ? 1 : -1;
|
|
24
|
+
const ax2end = x2 - (colGap / 2 - r);
|
|
25
|
+
const ax = ax2end - 2 * r;
|
|
26
|
+
const sw1 = dy > 0 ? 1 : 0;
|
|
27
|
+
const sw2 = dy > 0 ? 0 : 1;
|
|
28
|
+
return {
|
|
29
|
+
d: [
|
|
30
|
+
`M ${x1} ${y1}`,
|
|
31
|
+
`L ${ax} ${y1}`,
|
|
32
|
+
`A ${r} ${r} 0 0 ${sw1} ${ax + r} ${y1 + s * r}`,
|
|
33
|
+
`L ${ax + r} ${y2 - s * r}`,
|
|
34
|
+
`A ${r} ${r} 0 0 ${sw2} ${ax2end} ${y2}`,
|
|
35
|
+
`L ${x2} ${y2}`
|
|
36
|
+
].join(" "),
|
|
37
|
+
reversed: [
|
|
38
|
+
`M ${x2} ${y2}`,
|
|
39
|
+
`L ${ax2end} ${y2}`,
|
|
40
|
+
`A ${r} ${r} 0 0 ${sw1} ${ax + r} ${y2 - s * r}`,
|
|
41
|
+
`L ${ax + r} ${y1 + s * r}`,
|
|
42
|
+
`A ${r} ${r} 0 0 ${sw2} ${ax} ${y1}`,
|
|
43
|
+
`L ${x1} ${y1}`
|
|
44
|
+
].join(" ")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Whether two layouts put every node in the same place. */
|
|
48
|
+
function sameBoxes(a, b) {
|
|
49
|
+
if (a.size !== b.size) return false;
|
|
50
|
+
for (const [job, box] of a) {
|
|
51
|
+
const other = b.get(job);
|
|
52
|
+
if (!other || other.x !== box.x || other.y !== box.y || other.width !== box.width) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Every connector of a graph level, from its nodes' boxes, keyed by each node's first job. A node's
|
|
58
|
+
* jobs share their needs, so its first job's needs are the node's; a need naming a node not laid out
|
|
59
|
+
* draws nothing. `connectorY` is how far below a node's top its connectors meet it.
|
|
60
|
+
*/
|
|
61
|
+
function routeConnectors(cols, spaced, deps, boxes, connectorY) {
|
|
62
|
+
const heads = (c) => cols[c]?.[0]?.[0];
|
|
63
|
+
const gapAfter = (c) => {
|
|
64
|
+
const left = boxes.get(heads(c) ?? "");
|
|
65
|
+
const right = boxes.get(heads(c + 1) ?? "");
|
|
66
|
+
return left && right ? right.x - (left.x + left.width) : void 0;
|
|
67
|
+
};
|
|
68
|
+
let colGap = gapAfter(0) ?? 32;
|
|
69
|
+
if (spaced) for (let c = 1; c + 1 < cols.length; c++) colGap = Math.min(colGap, gapAfter(c) ?? colGap);
|
|
70
|
+
const ends = [];
|
|
71
|
+
let minRise = Number.POSITIVE_INFINITY;
|
|
72
|
+
for (const col of cols) for (const [to] of col) {
|
|
73
|
+
const target = to === void 0 ? void 0 : boxes.get(to);
|
|
74
|
+
if (to === void 0 || !target) continue;
|
|
75
|
+
for (const from of deps[to] ?? []) {
|
|
76
|
+
const source = boxes.get(from);
|
|
77
|
+
if (!source) continue;
|
|
78
|
+
const start = {
|
|
79
|
+
x: source.x + source.width - 2,
|
|
80
|
+
y: source.y + connectorY
|
|
81
|
+
};
|
|
82
|
+
const end = {
|
|
83
|
+
x: target.x + 2,
|
|
84
|
+
y: target.y + connectorY
|
|
85
|
+
};
|
|
86
|
+
const rise = Math.abs(end.y - start.y);
|
|
87
|
+
if (rise > 0) minRise = Math.min(minRise, rise);
|
|
88
|
+
ends.push({
|
|
89
|
+
from,
|
|
90
|
+
to,
|
|
91
|
+
start,
|
|
92
|
+
end
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const r = arcRadius(minRise, colGap);
|
|
97
|
+
return ends.map((edge) => ({
|
|
98
|
+
...edge,
|
|
99
|
+
path: connectorPath(edge.start, edge.end, r, colGap)
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { arcRadius, connectorPath, routeConnectors, sameBoxes };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GraphLayout } from '../editing.js';
|
|
2
|
+
import type { MatrixGroup, WorkflowEngine } from '../engine.js';
|
|
3
|
+
import { type WorkflowJob } from './types.js';
|
|
4
|
+
/** The jobs a level shows: those an `if` doesn't rule out, with each matrix collapsed into one job. */
|
|
5
|
+
export declare function displayJobsOf(engine: WorkflowEngine, jobs: Record<string, WorkflowJob>, matrixGroups: Record<string, MatrixGroup>): Record<string, WorkflowJob>;
|
|
6
|
+
export interface ComputedGraphLayout {
|
|
7
|
+
/** Columns of boxes, each box the jobs drawn as one node. */
|
|
8
|
+
dependencyCols: string[][][];
|
|
9
|
+
/** Each box's row; above a box, rows the layout leaves empty stay as space. */
|
|
10
|
+
rowSlots: number[][];
|
|
11
|
+
/** Each column's number; between two drawn columns, numbers skipped stay as space. */
|
|
12
|
+
colSlots: number[];
|
|
13
|
+
/** Each job's needs that draw a connector, sorted. */
|
|
14
|
+
directDeps: Record<string, string[]>;
|
|
15
|
+
/** The jobs that need each job directly, sorted. */
|
|
16
|
+
dependents: Record<string, string[]>;
|
|
17
|
+
displayJobs: Record<string, WorkflowJob>;
|
|
18
|
+
}
|
|
19
|
+
/** Levels `jobs` into columns of boxes, by their needs or by a stored layout. */
|
|
20
|
+
export declare function computeGraphLayout(engine: WorkflowEngine, jobs: Record<string, WorkflowJob>, matrixGroups: Record<string, MatrixGroup>, layout?: GraphLayout): ComputedGraphLayout;
|
|
21
|
+
/** Hops from `job` to each job it leads to or follows from, by breadth-first walks both ways. */
|
|
22
|
+
export declare function hopDistances(job: string, directDeps: Record<string, string[]>, dependents: Record<string, string[]>): Map<string, number>;
|
|
23
|
+
/** Each job's place on the graph from its `position`; none when no job has one. */
|
|
24
|
+
export declare function positionsOf(jobs: Record<string, unknown>): GraphLayout | undefined;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { isJobRecord } from "./types.js";
|
|
2
|
+
//#region src/graph/graphLayout.ts
|
|
3
|
+
/** The jobs a level shows: those an `if` doesn't rule out, with each matrix collapsed into one job. */
|
|
4
|
+
function displayJobsOf(engine, jobs, matrixGroups) {
|
|
5
|
+
const visibleJobs = {};
|
|
6
|
+
for (const [name, job] of Object.entries(jobs)) if (job.if !== false) visibleJobs[name] = { ...job };
|
|
7
|
+
engine.collapseMatrixGroups(matrixGroups, visibleJobs);
|
|
8
|
+
return visibleJobs;
|
|
9
|
+
}
|
|
10
|
+
function namedNeeds(engine, needs) {
|
|
11
|
+
return Array.isArray(needs) ? [...new Set(needs.map((need) => typeof need === "string" && engine.editing ? engine.editing.needTarget(need) : need))] : [];
|
|
12
|
+
}
|
|
13
|
+
function leadsTo(from, target, needs, column) {
|
|
14
|
+
const floor = column.get(target) ?? -1;
|
|
15
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
16
|
+
const stack = [from];
|
|
17
|
+
for (let job = stack.pop(); job !== void 0; job = stack.pop()) for (const need of needs[job] ?? []) {
|
|
18
|
+
if (need === target) return true;
|
|
19
|
+
if (!seen.has(need) && (column.get(need) ?? -1) > floor) {
|
|
20
|
+
seen.add(need);
|
|
21
|
+
stack.push(need);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function directNeeds(named, needs, column) {
|
|
27
|
+
if (named.length < 2) return named;
|
|
28
|
+
const at = (job) => column.get(job) ?? -1;
|
|
29
|
+
return named.filter((dep) => !named.some((other) => other !== dep && at(other) > at(dep) && leadsTo(other, dep, needs, column))).sort();
|
|
30
|
+
}
|
|
31
|
+
function columnsOf(names, deps) {
|
|
32
|
+
const visible = new Set(names);
|
|
33
|
+
const column = /* @__PURE__ */ new Map();
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const start of names) {
|
|
36
|
+
if (seen.has(start)) continue;
|
|
37
|
+
seen.add(start);
|
|
38
|
+
const stack = [[start, 0]];
|
|
39
|
+
while (stack.length > 0) {
|
|
40
|
+
const top = stack[stack.length - 1];
|
|
41
|
+
const [job, next] = top;
|
|
42
|
+
const needs = deps[job] ?? [];
|
|
43
|
+
const dep = needs[next];
|
|
44
|
+
if (dep !== void 0) {
|
|
45
|
+
top[1] += 1;
|
|
46
|
+
if (visible.has(dep) && !seen.has(dep)) {
|
|
47
|
+
seen.add(dep);
|
|
48
|
+
stack.push([dep, 0]);
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
stack.pop();
|
|
53
|
+
let at = needs.length === 0 ? 0 : 1;
|
|
54
|
+
let placed = true;
|
|
55
|
+
for (const need of needs) {
|
|
56
|
+
if (!visible.has(need)) continue;
|
|
57
|
+
const needAt = column.get(need);
|
|
58
|
+
if (needAt === void 0) {
|
|
59
|
+
placed = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
at = Math.max(at, needAt + 1);
|
|
63
|
+
}
|
|
64
|
+
if (placed) column.set(job, at);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return column;
|
|
68
|
+
}
|
|
69
|
+
function unitsOnly(columns, unitOf) {
|
|
70
|
+
return columns.map((col) => {
|
|
71
|
+
const boxes = [];
|
|
72
|
+
const rows = [];
|
|
73
|
+
let last = -1;
|
|
74
|
+
col.boxes.forEach((box, i) => {
|
|
75
|
+
const groups = [...groupBy(box, unitOf).values()];
|
|
76
|
+
const main = groups.reduce((a, b) => b.length > a.length ? b : a);
|
|
77
|
+
let row = Math.max(col.rows[i] ?? i, last + 1);
|
|
78
|
+
for (const group of [main, ...groups.filter((g) => g !== main)]) {
|
|
79
|
+
boxes.push(group);
|
|
80
|
+
rows.push(row);
|
|
81
|
+
last = row;
|
|
82
|
+
row += 1;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
...col,
|
|
87
|
+
boxes,
|
|
88
|
+
rows
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function groupBy(jobs, keyOf) {
|
|
93
|
+
const groups = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const job of jobs) {
|
|
95
|
+
const key = keyOf(job);
|
|
96
|
+
const group = groups.get(key);
|
|
97
|
+
if (group) group.push(job);
|
|
98
|
+
else groups.set(key, [job]);
|
|
99
|
+
}
|
|
100
|
+
return groups;
|
|
101
|
+
}
|
|
102
|
+
function withLoneMatrixSlots({ layoutPosition, mapLayout }, layout, jobs, matrixGroups) {
|
|
103
|
+
const lone = Object.entries(jobs).flatMap(([name, job]) => {
|
|
104
|
+
const original = job._matrix?.originaljob;
|
|
105
|
+
const slot = original && !matrixGroups[original] ? layoutPosition(layout, original) : void 0;
|
|
106
|
+
return slot && !layoutPosition(layout, name) ? [{
|
|
107
|
+
name,
|
|
108
|
+
slot
|
|
109
|
+
}] : [];
|
|
110
|
+
});
|
|
111
|
+
if (lone.length === 0) return layout;
|
|
112
|
+
const next = mapLayout(layout, (position) => position);
|
|
113
|
+
for (const { name, slot } of lone) next[name] = slot;
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
/** Levels `jobs` into columns of boxes, by their needs or by a stored layout. */
|
|
117
|
+
function computeGraphLayout(engine, jobs, matrixGroups, layout) {
|
|
118
|
+
const visibleJobs = displayJobsOf(engine, jobs, matrixGroups);
|
|
119
|
+
const names = Object.keys(visibleJobs);
|
|
120
|
+
const needs = {};
|
|
121
|
+
for (const name of names) needs[name] = namedNeeds(engine, visibleJobs[name]?.needs);
|
|
122
|
+
const column = columnsOf(names, needs);
|
|
123
|
+
const directDeps = {};
|
|
124
|
+
for (const name of names) directDeps[name] = directNeeds(needs[name] ?? [], needs, column);
|
|
125
|
+
const dependents = {};
|
|
126
|
+
for (const name of names) for (const dep of directDeps[name] ?? []) {
|
|
127
|
+
dependents[dep] ??= [];
|
|
128
|
+
dependents[dep].push(name);
|
|
129
|
+
}
|
|
130
|
+
for (const list of Object.values(dependents)) list.sort();
|
|
131
|
+
const units = /* @__PURE__ */ new Map();
|
|
132
|
+
const unitOf = (job) => {
|
|
133
|
+
let unit = units.get(job);
|
|
134
|
+
if (unit === void 0) {
|
|
135
|
+
unit = `${(directDeps[job] ?? []).join("\0")}\u0001${(dependents[job] ?? []).join("\0")}`;
|
|
136
|
+
units.set(job, unit);
|
|
137
|
+
}
|
|
138
|
+
return unit;
|
|
139
|
+
};
|
|
140
|
+
const alone = (job) => !!visibleJobs[job]?._matrixGroup;
|
|
141
|
+
const editing = engine.editing;
|
|
142
|
+
const laidOut = layout && editing?.hasLayout(layout) ? unitsOnly(editing.boxesFromLayout(names, directDeps, withLoneMatrixSlots(editing, layout, visibleJobs, matrixGroups), {
|
|
143
|
+
together: (a, b) => unitOf(a) === unitOf(b),
|
|
144
|
+
alone
|
|
145
|
+
}), unitOf) : null;
|
|
146
|
+
let dependencyCols;
|
|
147
|
+
if (laidOut) dependencyCols = laidOut.map((col) => col.boxes);
|
|
148
|
+
else {
|
|
149
|
+
const levels = [];
|
|
150
|
+
for (const [job, at] of column) {
|
|
151
|
+
levels[at] ??= [];
|
|
152
|
+
levels[at].push(job);
|
|
153
|
+
}
|
|
154
|
+
const order = new Map(names.map((name, i) => [name, i]));
|
|
155
|
+
const boxOf = (job) => alone(job) ? `\u0002${job}` : unitOf(job);
|
|
156
|
+
dependencyCols = levels.map((level) => [...groupBy(level.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)), boxOf).values()]);
|
|
157
|
+
if (dependencyCols[0] === void 0) dependencyCols.shift();
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
dependencyCols,
|
|
161
|
+
rowSlots: laidOut ? laidOut.map((col) => col.rows) : dependencyCols.map((col) => col.map((_, i) => i)),
|
|
162
|
+
colSlots: laidOut ? laidOut.map((col) => col.column) : dependencyCols.map((_, i) => i),
|
|
163
|
+
directDeps,
|
|
164
|
+
dependents,
|
|
165
|
+
displayJobs: visibleJobs
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Hops from `job` to each job it leads to or follows from, by breadth-first walks both ways. */
|
|
169
|
+
function hopDistances(job, directDeps, dependents) {
|
|
170
|
+
const dist = /* @__PURE__ */ new Map([[job, 0]]);
|
|
171
|
+
for (const edges of [directDeps, dependents]) {
|
|
172
|
+
const queue = [job];
|
|
173
|
+
for (let i = 0; i < queue.length; i++) {
|
|
174
|
+
const at = queue[i];
|
|
175
|
+
const d = (dist.get(at) ?? 0) + 1;
|
|
176
|
+
for (const next of edges[at] ?? []) if (!dist.has(next)) {
|
|
177
|
+
dist.set(next, d);
|
|
178
|
+
queue.push(next);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return dist;
|
|
183
|
+
}
|
|
184
|
+
var isSlot = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
185
|
+
/** Each job's place on the graph from its `position`; none when no job has one. */
|
|
186
|
+
function positionsOf(jobs) {
|
|
187
|
+
const layout = Object.create(null);
|
|
188
|
+
let any = false;
|
|
189
|
+
for (const [name, job] of Object.entries(jobs)) {
|
|
190
|
+
const { position, _matrix } = isJobRecord(job) ? job : {};
|
|
191
|
+
const { column, row } = isJobRecord(position) ? position : {};
|
|
192
|
+
const original = isJobRecord(_matrix) ? _matrix["originaljob"] : void 0;
|
|
193
|
+
const key = typeof original === "string" && original ? original : name;
|
|
194
|
+
if (isSlot(column) && isSlot(row) && !(key in layout)) {
|
|
195
|
+
layout[key] = {
|
|
196
|
+
column,
|
|
197
|
+
row
|
|
198
|
+
};
|
|
199
|
+
any = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return any ? layout : void 0;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
export { computeGraphLayout, displayJobsOf, hopDistances, positionsOf };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Store } from './store.js';
|
|
2
|
+
/**
|
|
3
|
+
* The hovered job's lineage on one graph level: the jobs it leads to or follows from, by hops. Each
|
|
4
|
+
* node, connector and dot reads only its own part of it, so hovering re-renders those whose highlight
|
|
5
|
+
* changes and nothing else, however large the graph.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Lineage extends Store {
|
|
8
|
+
/** Hops from the hovered job, or none while no job is. */
|
|
9
|
+
private hovered;
|
|
10
|
+
/** The lineage shown, kept after unhover while its highlight draws back out. */
|
|
11
|
+
private shown;
|
|
12
|
+
private furthest;
|
|
13
|
+
private timer;
|
|
14
|
+
private readonly walk;
|
|
15
|
+
/** Seconds each hop of the highlight takes. */
|
|
16
|
+
readonly animT: number;
|
|
17
|
+
/** How deeply the level nests, which names the classes that dim it. */
|
|
18
|
+
readonly depth: number;
|
|
19
|
+
/** Whether its nodes fade as they dim, rather than dim at once. */
|
|
20
|
+
readonly fade: boolean;
|
|
21
|
+
constructor(walk: (job: string) => Map<string, number>, { animT, depth, fade }: {
|
|
22
|
+
animT: number;
|
|
23
|
+
depth?: number;
|
|
24
|
+
fade?: boolean;
|
|
25
|
+
});
|
|
26
|
+
get hovering(): boolean;
|
|
27
|
+
/** Whether a lineage shows, hovered or drawing out. */
|
|
28
|
+
get showing(): boolean;
|
|
29
|
+
hover: (job: string | null) => void;
|
|
30
|
+
/** Stops a draw-out still to finish, as the level goes. */
|
|
31
|
+
dispose(): void;
|
|
32
|
+
/** A node's highlight: 2 in the hovered lineage, 1 drawing out of it, 0 neither. */
|
|
33
|
+
node(job: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* A dot's or connector's highlight between `from` and `to` (a dot's both the same job), as one
|
|
36
|
+
* value React compares: none, or lit or drawing out, its delay, and whether it draws from `to`.
|
|
37
|
+
*/
|
|
38
|
+
between(from: string, to: string): string;
|
|
39
|
+
}
|
|
40
|
+
/** A highlight `between` describes, read back. */
|
|
41
|
+
export declare function highlightOf(value: string): {
|
|
42
|
+
lit: boolean;
|
|
43
|
+
out: boolean;
|
|
44
|
+
delay: number;
|
|
45
|
+
back: boolean;
|
|
46
|
+
};
|
|
47
|
+
export declare const LineageContext: import("react").Context<Lineage | null>;
|
|
48
|
+
/** Part of the enclosing level's lineage, re-rendering only when that part changes. */
|
|
49
|
+
export declare function useLineage<T extends string | number>(read: (lineage: Lineage) => T, none: T): T;
|
|
50
|
+
/** The enclosing level's lineage, for what hovers it. */
|
|
51
|
+
export declare function useLineageStore(): Lineage | null;
|