@principal-ai/principal-view-react 0.7.39 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/orientationUtils.d.ts +19 -0
- package/dist/utils/orientationUtils.d.ts.map +1 -0
- package/dist/utils/orientationUtils.js +62 -0
- package/dist/utils/orientationUtils.js.map +1 -0
- package/package.json +1 -2
- package/src/index.ts +5 -0
- package/src/stories/GraphOrientation.stories.tsx +500 -0
- package/src/utils/orientationUtils.ts +73 -0
- package/src/stories/RealTestExecution.stories.tsx +0 -404
- package/src/stories/ValidatedExecution.stories.tsx +0 -158
package/dist/index.d.ts
CHANGED
|
@@ -28,4 +28,5 @@ export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConver
|
|
|
28
28
|
export type { EdgeStateWithHandles } from './utils/graphConverter';
|
|
29
29
|
export { Icon, resolveIcon } from './utils/iconResolver';
|
|
30
30
|
export type { IconProps } from './utils/iconResolver';
|
|
31
|
+
export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
|
|
31
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,4 +20,5 @@ export { NodeTooltip } from './components/NodeTooltip';
|
|
|
20
20
|
// Export utilities
|
|
21
21
|
export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConverter';
|
|
22
22
|
export { Icon, resolveIcon } from './utils/iconResolver';
|
|
23
|
+
export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
|
|
23
24
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NodeState, EdgeState } from '@principal-ai/principal-view-core';
|
|
2
|
+
/**
|
|
3
|
+
* Swaps the orientation of a graph from horizontal to vertical or vice versa.
|
|
4
|
+
* This swaps x/y coordinates and adjusts edge connection sides accordingly.
|
|
5
|
+
*/
|
|
6
|
+
export declare function swapGraphOrientation(nodes: NodeState[], edges: EdgeState[]): {
|
|
7
|
+
nodes: NodeState[];
|
|
8
|
+
edges: EdgeState[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Swaps x and y positions for all nodes
|
|
12
|
+
*/
|
|
13
|
+
export declare function swapNodePositions(nodes: NodeState[]): NodeState[];
|
|
14
|
+
/**
|
|
15
|
+
* Swaps edge connection sides to match the new orientation
|
|
16
|
+
* Both sides rotate clockwise: top → right → bottom → left → top
|
|
17
|
+
*/
|
|
18
|
+
export declare function swapEdgeSides(edges: EdgeState[]): EdgeState[];
|
|
19
|
+
//# sourceMappingURL=orientationUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orientationUtils.d.ts","sourceRoot":"","sources":["../../src/utils/orientationUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAc,MAAM,mCAAmC,CAAC;AAE1F;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,SAAS,EAAE,GACjB;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAK5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAQjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAuB7D"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swaps the orientation of a graph from horizontal to vertical or vice versa.
|
|
3
|
+
* This swaps x/y coordinates and adjusts edge connection sides accordingly.
|
|
4
|
+
*/
|
|
5
|
+
export function swapGraphOrientation(nodes, edges) {
|
|
6
|
+
return {
|
|
7
|
+
nodes: swapNodePositions(nodes),
|
|
8
|
+
edges: swapEdgeSides(edges),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Swaps x and y positions for all nodes
|
|
13
|
+
*/
|
|
14
|
+
export function swapNodePositions(nodes) {
|
|
15
|
+
return nodes.map(node => ({
|
|
16
|
+
...node,
|
|
17
|
+
position: node.position ? {
|
|
18
|
+
x: node.position.y,
|
|
19
|
+
y: node.position.x,
|
|
20
|
+
} : undefined,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Swaps edge connection sides to match the new orientation
|
|
25
|
+
* Both sides rotate clockwise: top → right → bottom → left → top
|
|
26
|
+
*/
|
|
27
|
+
export function swapEdgeSides(edges) {
|
|
28
|
+
return edges.map(edge => {
|
|
29
|
+
const data = edge.data;
|
|
30
|
+
const fromSide = data?.fromSide;
|
|
31
|
+
const toSide = data?.toSide;
|
|
32
|
+
const rotatedFromSide = rotateClockwise(fromSide);
|
|
33
|
+
const rotatedToSide = rotateClockwise(toSide);
|
|
34
|
+
// Only include fromSide/toSide if they have defined values (not undefined)
|
|
35
|
+
const newData = { ...edge.data };
|
|
36
|
+
if (rotatedFromSide !== undefined) {
|
|
37
|
+
newData.fromSide = rotatedFromSide;
|
|
38
|
+
}
|
|
39
|
+
if (rotatedToSide !== undefined) {
|
|
40
|
+
newData.toSide = rotatedToSide;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...edge,
|
|
44
|
+
data: newData,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Rotates a side clockwise: top → right → bottom → left → top
|
|
50
|
+
*/
|
|
51
|
+
function rotateClockwise(side) {
|
|
52
|
+
if (!side)
|
|
53
|
+
return undefined;
|
|
54
|
+
const clockwiseMap = {
|
|
55
|
+
top: 'right',
|
|
56
|
+
right: 'bottom',
|
|
57
|
+
bottom: 'left',
|
|
58
|
+
left: 'top',
|
|
59
|
+
};
|
|
60
|
+
return clockwiseMap[side];
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=orientationUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orientationUtils.js","sourceRoot":"","sources":["../../src/utils/orientationUtils.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAkB,EAClB,KAAkB;IAElB,OAAO;QACL,KAAK,EAAE,iBAAiB,CAAC,KAAK,CAAC;QAC/B,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAkB;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxB,GAAG,IAAI;QACP,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YACxB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;SACnB,CAAC,CAAC,CAAC,SAAS;KACd,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB;IAC9C,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAA2C,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAkC,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,EAAE,MAAgC,CAAC;QAEtD,MAAM,eAAe,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QAE9C,2EAA2E;QAC3E,MAAM,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAA6B,CAAC;QAC5D,IAAI,eAAe,KAAK,SAAS,EAAE;YACjC,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC;SACpC;QACD,IAAI,aAAa,KAAK,SAAS,EAAE;YAC/B,OAAO,CAAC,MAAM,GAAG,aAAa,CAAC;SAChC;QAED,OAAO;YACL,GAAG,IAAI;YACP,IAAI,EAAE,OAA4B;SACnC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,IAAiB;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAE5B,MAAM,YAAY,GAAmC;QACnD,GAAG,EAAE,OAAO;QACZ,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,KAAK;KACZ,CAAC;IAEF,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principal-ai/principal-view-react",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.40",
|
|
4
4
|
"description": "React components for graph-based principal view framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"@principal-ai/codebase-composition": "^0.2.41",
|
|
19
19
|
"@principal-ai/principal-view-core": "0.11.7",
|
|
20
20
|
"@xyflow/react": "12.0.0",
|
|
21
|
-
"elkjs": "0.9.0",
|
|
22
21
|
"js-yaml": "4.1.1",
|
|
23
22
|
"lucide-react": "0.554.0",
|
|
24
23
|
"zustand": "4.5.0"
|
package/src/index.ts
CHANGED
|
@@ -71,3 +71,8 @@ export {
|
|
|
71
71
|
export type { EdgeStateWithHandles } from './utils/graphConverter';
|
|
72
72
|
export { Icon, resolveIcon } from './utils/iconResolver';
|
|
73
73
|
export type { IconProps } from './utils/iconResolver';
|
|
74
|
+
export {
|
|
75
|
+
swapGraphOrientation,
|
|
76
|
+
swapNodePositions,
|
|
77
|
+
swapEdgeSides,
|
|
78
|
+
} from './utils/orientationUtils';
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { GraphRenderer } from '../components/GraphRenderer';
|
|
4
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core/browser';
|
|
5
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
6
|
+
import { swapGraphOrientation } from '../utils/orientationUtils';
|
|
7
|
+
import { CanvasConverter } from '@principal-ai/principal-view-core/browser';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Features/Graph Orientation',
|
|
11
|
+
component: GraphRenderer,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
19
|
+
<Story />
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
} satisfies Meta<typeof GraphRenderer>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Canvas with horizontal flow (left to right)
|
|
30
|
+
*/
|
|
31
|
+
const horizontalCanvas: ExtendedCanvas = {
|
|
32
|
+
nodes: [
|
|
33
|
+
{
|
|
34
|
+
id: 'start',
|
|
35
|
+
type: 'text',
|
|
36
|
+
x: 100,
|
|
37
|
+
y: 200,
|
|
38
|
+
width: 120,
|
|
39
|
+
height: 80,
|
|
40
|
+
text: 'Start',
|
|
41
|
+
color: 4,
|
|
42
|
+
pv: {
|
|
43
|
+
nodeType: 'process',
|
|
44
|
+
shape: 'circle',
|
|
45
|
+
icon: 'Play',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'process-1',
|
|
50
|
+
type: 'text',
|
|
51
|
+
x: 300,
|
|
52
|
+
y: 200,
|
|
53
|
+
width: 140,
|
|
54
|
+
height: 80,
|
|
55
|
+
text: 'Process 1',
|
|
56
|
+
color: 6,
|
|
57
|
+
pv: {
|
|
58
|
+
nodeType: 'process',
|
|
59
|
+
shape: 'rectangle',
|
|
60
|
+
icon: 'Box',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'decision',
|
|
65
|
+
type: 'text',
|
|
66
|
+
x: 520,
|
|
67
|
+
y: 200,
|
|
68
|
+
width: 100,
|
|
69
|
+
height: 100,
|
|
70
|
+
text: 'Decision',
|
|
71
|
+
color: 2,
|
|
72
|
+
pv: {
|
|
73
|
+
nodeType: 'decision',
|
|
74
|
+
shape: 'diamond',
|
|
75
|
+
icon: 'GitBranch',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'process-2a',
|
|
80
|
+
type: 'text',
|
|
81
|
+
x: 700,
|
|
82
|
+
y: 120,
|
|
83
|
+
width: 140,
|
|
84
|
+
height: 80,
|
|
85
|
+
text: 'Process 2A',
|
|
86
|
+
color: 6,
|
|
87
|
+
pv: {
|
|
88
|
+
nodeType: 'process',
|
|
89
|
+
shape: 'rectangle',
|
|
90
|
+
icon: 'Box',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'process-2b',
|
|
95
|
+
type: 'text',
|
|
96
|
+
x: 700,
|
|
97
|
+
y: 280,
|
|
98
|
+
width: 140,
|
|
99
|
+
height: 80,
|
|
100
|
+
text: 'Process 2B',
|
|
101
|
+
color: 6,
|
|
102
|
+
pv: {
|
|
103
|
+
nodeType: 'process',
|
|
104
|
+
shape: 'rectangle',
|
|
105
|
+
icon: 'Box',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'end',
|
|
110
|
+
type: 'text',
|
|
111
|
+
x: 920,
|
|
112
|
+
y: 200,
|
|
113
|
+
width: 120,
|
|
114
|
+
height: 80,
|
|
115
|
+
text: 'End',
|
|
116
|
+
color: 5,
|
|
117
|
+
pv: {
|
|
118
|
+
nodeType: 'process',
|
|
119
|
+
shape: 'circle',
|
|
120
|
+
icon: 'CheckCircle',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
edges: [
|
|
125
|
+
{
|
|
126
|
+
id: 'e1',
|
|
127
|
+
fromNode: 'start',
|
|
128
|
+
toNode: 'process-1',
|
|
129
|
+
fromSide: 'right',
|
|
130
|
+
toSide: 'left',
|
|
131
|
+
pv: {
|
|
132
|
+
edgeType: 'flow',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'e2',
|
|
137
|
+
fromNode: 'process-1',
|
|
138
|
+
toNode: 'decision',
|
|
139
|
+
fromSide: 'right',
|
|
140
|
+
toSide: 'left',
|
|
141
|
+
pv: {
|
|
142
|
+
edgeType: 'flow',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'e3',
|
|
147
|
+
fromNode: 'decision',
|
|
148
|
+
toNode: 'process-2a',
|
|
149
|
+
fromSide: 'top',
|
|
150
|
+
toSide: 'left',
|
|
151
|
+
pv: {
|
|
152
|
+
edgeType: 'flow',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'e4',
|
|
157
|
+
fromNode: 'decision',
|
|
158
|
+
toNode: 'process-2b',
|
|
159
|
+
fromSide: 'bottom',
|
|
160
|
+
toSide: 'left',
|
|
161
|
+
pv: {
|
|
162
|
+
edgeType: 'flow',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'e5',
|
|
167
|
+
fromNode: 'process-2a',
|
|
168
|
+
toNode: 'end',
|
|
169
|
+
fromSide: 'right',
|
|
170
|
+
toSide: 'top',
|
|
171
|
+
pv: {
|
|
172
|
+
edgeType: 'flow',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'e6',
|
|
177
|
+
fromNode: 'process-2b',
|
|
178
|
+
toNode: 'end',
|
|
179
|
+
fromSide: 'right',
|
|
180
|
+
toSide: 'bottom',
|
|
181
|
+
pv: {
|
|
182
|
+
edgeType: 'flow',
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
pv: {
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
name: 'Horizontal Flow',
|
|
189
|
+
description: 'Graph with horizontal (left-to-right) orientation',
|
|
190
|
+
edgeTypes: {
|
|
191
|
+
flow: {
|
|
192
|
+
description: 'Flow connection',
|
|
193
|
+
style: 'solid',
|
|
194
|
+
color: '#888',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Interactive story demonstrating orientation swap
|
|
202
|
+
*/
|
|
203
|
+
const OrientationSwapTemplate = () => {
|
|
204
|
+
const [isVertical, setIsVertical] = useState(false);
|
|
205
|
+
|
|
206
|
+
const canvas = useMemo(() => {
|
|
207
|
+
if (!isVertical) {
|
|
208
|
+
return horizontalCanvas;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert canvas to internal format
|
|
212
|
+
const { nodes: nodeStates, edges: edgeStates } =
|
|
213
|
+
CanvasConverter.canvasToGraph(horizontalCanvas);
|
|
214
|
+
|
|
215
|
+
// Swap orientation
|
|
216
|
+
const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
|
|
217
|
+
nodeStates,
|
|
218
|
+
edgeStates
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Convert back to canvas format for rendering
|
|
222
|
+
return {
|
|
223
|
+
...horizontalCanvas,
|
|
224
|
+
nodes: swappedNodes.map((node, idx) => ({
|
|
225
|
+
...horizontalCanvas.nodes[idx],
|
|
226
|
+
x: node.position?.x ?? 0,
|
|
227
|
+
y: node.position?.y ?? 0,
|
|
228
|
+
})),
|
|
229
|
+
edges: swappedEdges.map((edge, idx) => {
|
|
230
|
+
const data = edge.data as Record<string, unknown> | undefined;
|
|
231
|
+
return {
|
|
232
|
+
...horizontalCanvas.edges[idx],
|
|
233
|
+
fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
|
|
234
|
+
toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
|
|
235
|
+
};
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
}, [isVertical]);
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div style={{ padding: 20 }}>
|
|
242
|
+
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 20 }}>
|
|
243
|
+
<h2 style={{ margin: 0, fontFamily: 'system-ui' }}>Graph Orientation</h2>
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => setIsVertical(!isVertical)}
|
|
246
|
+
style={{
|
|
247
|
+
padding: '8px 16px',
|
|
248
|
+
fontSize: 14,
|
|
249
|
+
fontFamily: 'system-ui',
|
|
250
|
+
backgroundColor: '#3b82f6',
|
|
251
|
+
color: 'white',
|
|
252
|
+
border: 'none',
|
|
253
|
+
borderRadius: 6,
|
|
254
|
+
cursor: 'pointer',
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
Switch to {isVertical ? 'Horizontal' : 'Vertical'}
|
|
258
|
+
</button>
|
|
259
|
+
<div style={{ fontSize: 14, color: '#666', fontFamily: 'system-ui' }}>
|
|
260
|
+
Current: <strong>{isVertical ? 'Vertical (Top-to-Bottom)' : 'Horizontal (Left-to-Right)'}</strong>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<GraphRenderer
|
|
265
|
+
key={isVertical ? 'vertical' : 'horizontal'}
|
|
266
|
+
canvas={canvas}
|
|
267
|
+
width={isVertical ? 500 : 1100}
|
|
268
|
+
height={isVertical ? 1100 : 500}
|
|
269
|
+
/>
|
|
270
|
+
|
|
271
|
+
<div style={{ marginTop: 30, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 8 }}>
|
|
272
|
+
<h4 style={{ marginBottom: 10, fontFamily: 'system-ui' }}>How It Works</h4>
|
|
273
|
+
<ul style={{ fontSize: 13, lineHeight: 1.8, margin: 0, paddingLeft: 20 }}>
|
|
274
|
+
<li>
|
|
275
|
+
<strong>Position Swap</strong>: Swaps x and y coordinates of all nodes
|
|
276
|
+
</li>
|
|
277
|
+
<li>
|
|
278
|
+
<strong>Edge Side Rotation</strong>: Both sides rotate <strong>clockwise</strong>
|
|
279
|
+
<ul style={{ marginTop: 5 }}>
|
|
280
|
+
<li><code>top → right → bottom → left → top</code></li>
|
|
281
|
+
<li>Example: <code>right → left</code> becomes <code>bottom → top</code></li>
|
|
282
|
+
</ul>
|
|
283
|
+
</li>
|
|
284
|
+
<li>
|
|
285
|
+
<strong>Usage</strong>: <code>swapGraphOrientation(nodes, edges)</code>
|
|
286
|
+
</li>
|
|
287
|
+
</ul>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const InteractiveOrientationSwap: Story = {
|
|
294
|
+
render: () => <OrientationSwapTemplate />,
|
|
295
|
+
parameters: {
|
|
296
|
+
docs: {
|
|
297
|
+
description: {
|
|
298
|
+
story: `
|
|
299
|
+
**Interactive Orientation Swap**
|
|
300
|
+
|
|
301
|
+
Click the button to toggle between horizontal (left-to-right) and vertical (top-to-bottom) orientations.
|
|
302
|
+
|
|
303
|
+
The \`swapGraphOrientation\` utility:
|
|
304
|
+
1. Swaps x/y coordinates for all node positions
|
|
305
|
+
2. Rotates edge connection sides clockwise (both fromSide and toSide)
|
|
306
|
+
- Rotation: top → right → bottom → left → top
|
|
307
|
+
- Example: \`right → left\` becomes \`bottom → top\`
|
|
308
|
+
3. Preserves the graph structure while rotating it 90 degrees
|
|
309
|
+
`,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Horizontal flow example
|
|
317
|
+
*/
|
|
318
|
+
export const HorizontalFlow: Story = {
|
|
319
|
+
args: {
|
|
320
|
+
canvas: horizontalCanvas,
|
|
321
|
+
width: 1100,
|
|
322
|
+
height: 450,
|
|
323
|
+
},
|
|
324
|
+
parameters: {
|
|
325
|
+
docs: {
|
|
326
|
+
description: {
|
|
327
|
+
story: `
|
|
328
|
+
**Horizontal Flow (Left-to-Right)**
|
|
329
|
+
|
|
330
|
+
Traditional left-to-right flowchart layout:
|
|
331
|
+
- Primary flow uses \`right → left\` connections
|
|
332
|
+
- Branches use \`top\` and \`bottom\` connections for splitting
|
|
333
|
+
- Typical for flowcharts and process diagrams
|
|
334
|
+
`,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Vertical flow example (using swapped canvas)
|
|
342
|
+
*/
|
|
343
|
+
const VerticalFlowTemplate = () => {
|
|
344
|
+
const verticalCanvas = useMemo(() => {
|
|
345
|
+
const { nodes: nodeStates, edges: edgeStates } =
|
|
346
|
+
CanvasConverter.canvasToGraph(horizontalCanvas);
|
|
347
|
+
|
|
348
|
+
const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
|
|
349
|
+
nodeStates,
|
|
350
|
+
edgeStates
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
...horizontalCanvas,
|
|
355
|
+
pv: {
|
|
356
|
+
...horizontalCanvas.pv,
|
|
357
|
+
name: 'Vertical Flow',
|
|
358
|
+
description: 'Graph with vertical (top-to-bottom) orientation',
|
|
359
|
+
},
|
|
360
|
+
nodes: swappedNodes.map((node, idx) => ({
|
|
361
|
+
...horizontalCanvas.nodes[idx],
|
|
362
|
+
x: node.position?.x ?? 0,
|
|
363
|
+
y: node.position?.y ?? 0,
|
|
364
|
+
})),
|
|
365
|
+
edges: swappedEdges.map((edge, idx) => {
|
|
366
|
+
const data = edge.data as Record<string, unknown> | undefined;
|
|
367
|
+
return {
|
|
368
|
+
...horizontalCanvas.edges[idx],
|
|
369
|
+
fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
|
|
370
|
+
toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
return <GraphRenderer canvas={verticalCanvas} width={500} height={1100} />;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export const VerticalFlow: Story = {
|
|
380
|
+
render: () => <VerticalFlowTemplate />,
|
|
381
|
+
parameters: {
|
|
382
|
+
docs: {
|
|
383
|
+
description: {
|
|
384
|
+
story: `
|
|
385
|
+
**Vertical Flow (Top-to-Bottom)**
|
|
386
|
+
|
|
387
|
+
Top-to-bottom layout using \`swapGraphOrientation\`:
|
|
388
|
+
- Primary flow uses \`bottom → top\` connections
|
|
389
|
+
- Branches use \`left\` and \`right\` connections for splitting
|
|
390
|
+
- Common for hierarchical diagrams and org charts
|
|
391
|
+
`,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Side-by-side comparison
|
|
399
|
+
*/
|
|
400
|
+
const SideBySideTemplate = () => {
|
|
401
|
+
const verticalCanvas = useMemo(() => {
|
|
402
|
+
const { nodes: nodeStates, edges: edgeStates } =
|
|
403
|
+
CanvasConverter.canvasToGraph(horizontalCanvas);
|
|
404
|
+
|
|
405
|
+
const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
|
|
406
|
+
nodeStates,
|
|
407
|
+
edgeStates
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
...horizontalCanvas,
|
|
412
|
+
nodes: swappedNodes.map((node, idx) => ({
|
|
413
|
+
...horizontalCanvas.nodes[idx],
|
|
414
|
+
x: node.position?.x ?? 0,
|
|
415
|
+
y: node.position?.y ?? 0,
|
|
416
|
+
})),
|
|
417
|
+
edges: swappedEdges.map((edge, idx) => {
|
|
418
|
+
const data = edge.data as Record<string, unknown> | undefined;
|
|
419
|
+
return {
|
|
420
|
+
...horizontalCanvas.edges[idx],
|
|
421
|
+
fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
|
|
422
|
+
toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
|
|
423
|
+
};
|
|
424
|
+
}),
|
|
425
|
+
};
|
|
426
|
+
}, []);
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div style={{ padding: 20 }}>
|
|
430
|
+
<h2 style={{ marginBottom: 20, fontFamily: 'system-ui' }}>Orientation Comparison</h2>
|
|
431
|
+
|
|
432
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40 }}>
|
|
433
|
+
<div>
|
|
434
|
+
<h3 style={{ fontFamily: 'system-ui', marginBottom: 10 }}>
|
|
435
|
+
Horizontal (Left-to-Right)
|
|
436
|
+
</h3>
|
|
437
|
+
<div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
|
|
438
|
+
<GraphRenderer canvas={horizontalCanvas} width={550} height={400} />
|
|
439
|
+
</div>
|
|
440
|
+
<div style={{ marginTop: 10, fontSize: 13, color: '#666' }}>
|
|
441
|
+
Original orientation with right→left primary flow
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div>
|
|
446
|
+
<h3 style={{ fontFamily: 'system-ui', marginBottom: 10 }}>
|
|
447
|
+
Vertical (Top-to-Bottom)
|
|
448
|
+
</h3>
|
|
449
|
+
<div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
|
|
450
|
+
<GraphRenderer canvas={verticalCanvas} width={550} height={400} />
|
|
451
|
+
</div>
|
|
452
|
+
<div style={{ marginTop: 10, fontSize: 13, color: '#666' }}>
|
|
453
|
+
Swapped orientation with bottom→top primary flow
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div style={{ marginTop: 30, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 8 }}>
|
|
459
|
+
<h4 style={{ marginBottom: 10, fontFamily: 'system-ui' }}>
|
|
460
|
+
Edge Side Rotation (Both Clockwise)
|
|
461
|
+
</h4>
|
|
462
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
|
463
|
+
<div>
|
|
464
|
+
<strong>Clockwise Rotation:</strong>
|
|
465
|
+
<ul style={{ fontSize: 13, lineHeight: 1.8, marginTop: 8, paddingLeft: 20 }}>
|
|
466
|
+
<li><code>top</code> → <code>right</code></li>
|
|
467
|
+
<li><code>right</code> → <code>bottom</code></li>
|
|
468
|
+
<li><code>bottom</code> → <code>left</code></li>
|
|
469
|
+
<li><code>left</code> → <code>top</code></li>
|
|
470
|
+
</ul>
|
|
471
|
+
</div>
|
|
472
|
+
<div>
|
|
473
|
+
<strong>Examples:</strong>
|
|
474
|
+
<ul style={{ fontSize: 13, lineHeight: 1.8, marginTop: 8, paddingLeft: 20 }}>
|
|
475
|
+
<li><code>right → left</code> becomes <code>bottom → top</code></li>
|
|
476
|
+
<li><code>top → left</code> becomes <code>right → top</code></li>
|
|
477
|
+
<li><code>bottom → left</code> becomes <code>left → top</code></li>
|
|
478
|
+
</ul>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export const SideBySideComparison: Story = {
|
|
487
|
+
render: () => <SideBySideTemplate />,
|
|
488
|
+
parameters: {
|
|
489
|
+
docs: {
|
|
490
|
+
description: {
|
|
491
|
+
story: `
|
|
492
|
+
**Side-by-Side Comparison**
|
|
493
|
+
|
|
494
|
+
Visual comparison of the same graph in both orientations, showing how \`swapGraphOrientation\`
|
|
495
|
+
transforms the layout while preserving the graph structure and flow logic.
|
|
496
|
+
`,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { NodeState, EdgeState, CanvasSide } from '@principal-ai/principal-view-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Swaps the orientation of a graph from horizontal to vertical or vice versa.
|
|
5
|
+
* This swaps x/y coordinates and adjusts edge connection sides accordingly.
|
|
6
|
+
*/
|
|
7
|
+
export function swapGraphOrientation(
|
|
8
|
+
nodes: NodeState[],
|
|
9
|
+
edges: EdgeState[]
|
|
10
|
+
): { nodes: NodeState[]; edges: EdgeState[] } {
|
|
11
|
+
return {
|
|
12
|
+
nodes: swapNodePositions(nodes),
|
|
13
|
+
edges: swapEdgeSides(edges),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Swaps x and y positions for all nodes
|
|
19
|
+
*/
|
|
20
|
+
export function swapNodePositions(nodes: NodeState[]): NodeState[] {
|
|
21
|
+
return nodes.map(node => ({
|
|
22
|
+
...node,
|
|
23
|
+
position: node.position ? {
|
|
24
|
+
x: node.position.y,
|
|
25
|
+
y: node.position.x,
|
|
26
|
+
} : undefined,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Swaps edge connection sides to match the new orientation
|
|
32
|
+
* Both sides rotate clockwise: top → right → bottom → left → top
|
|
33
|
+
*/
|
|
34
|
+
export function swapEdgeSides(edges: EdgeState[]): EdgeState[] {
|
|
35
|
+
return edges.map(edge => {
|
|
36
|
+
const data = edge.data as Record<string, unknown> | undefined;
|
|
37
|
+
const fromSide = data?.fromSide as CanvasSide | undefined;
|
|
38
|
+
const toSide = data?.toSide as CanvasSide | undefined;
|
|
39
|
+
|
|
40
|
+
const rotatedFromSide = rotateClockwise(fromSide);
|
|
41
|
+
const rotatedToSide = rotateClockwise(toSide);
|
|
42
|
+
|
|
43
|
+
// Only include fromSide/toSide if they have defined values (not undefined)
|
|
44
|
+
const newData = { ...edge.data } as Record<string, unknown>;
|
|
45
|
+
if (rotatedFromSide !== undefined) {
|
|
46
|
+
newData.fromSide = rotatedFromSide;
|
|
47
|
+
}
|
|
48
|
+
if (rotatedToSide !== undefined) {
|
|
49
|
+
newData.toSide = rotatedToSide;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...edge,
|
|
54
|
+
data: newData as EdgeState['data'],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rotates a side clockwise: top → right → bottom → left → top
|
|
61
|
+
*/
|
|
62
|
+
function rotateClockwise(side?: CanvasSide): CanvasSide | undefined {
|
|
63
|
+
if (!side) return undefined;
|
|
64
|
+
|
|
65
|
+
const clockwiseMap: Record<CanvasSide, CanvasSide> = {
|
|
66
|
+
top: 'right',
|
|
67
|
+
right: 'bottom',
|
|
68
|
+
bottom: 'left',
|
|
69
|
+
left: 'top',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return clockwiseMap[side];
|
|
73
|
+
}
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import { GraphRenderer } from '../components/GraphRenderer';
|
|
4
|
-
import { TestEventPanel } from '../components/TestEventPanel';
|
|
5
|
-
import type { ViewMode } from '../components/TestEventPanel';
|
|
6
|
-
import type { ExtendedCanvas, GraphEvent, JsonObject, NarrativeTemplate } from '@principal-ai/principal-view-core/browser';
|
|
7
|
-
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
8
|
-
import testSpans from './data/graph-converter-test-execution.json';
|
|
9
|
-
import narrativeTemplate from './data/graph-converter-test.narrative.json';
|
|
10
|
-
|
|
11
|
-
const meta = {
|
|
12
|
-
title: 'Features/Real Test Execution',
|
|
13
|
-
component: GraphRenderer,
|
|
14
|
-
parameters: {
|
|
15
|
-
layout: 'fullscreen',
|
|
16
|
-
docs: {
|
|
17
|
-
description: {
|
|
18
|
-
component:
|
|
19
|
-
'Visualizes REAL test execution data from instrumented Bun tests using the "wide event" pattern. Shows actual spans with file/line information collected from running GraphConverter.test.ts. Hover over graph nodes to highlight related events in the panel.',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
tags: ['autodocs'],
|
|
24
|
-
decorators: [
|
|
25
|
-
(Story) => (
|
|
26
|
-
<ThemeProvider theme={defaultEditorTheme}>
|
|
27
|
-
<div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
|
|
28
|
-
<Story />
|
|
29
|
-
</div>
|
|
30
|
-
</ThemeProvider>
|
|
31
|
-
),
|
|
32
|
-
],
|
|
33
|
-
} satisfies Meta<typeof GraphRenderer>;
|
|
34
|
-
|
|
35
|
-
export default meta;
|
|
36
|
-
type Story = StoryObj<typeof meta>;
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Test Execution Flow Canvas
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
const testExecutionCanvas: ExtendedCanvas = {
|
|
43
|
-
nodes: [
|
|
44
|
-
// Test Suite
|
|
45
|
-
{
|
|
46
|
-
id: 'test-suite',
|
|
47
|
-
type: 'text',
|
|
48
|
-
text: 'GraphConverter Test Suite',
|
|
49
|
-
x: -100,
|
|
50
|
-
y: -100,
|
|
51
|
-
width: 240,
|
|
52
|
-
height: 80,
|
|
53
|
-
pv: {
|
|
54
|
-
nodeType: 'test-suite',
|
|
55
|
-
name: 'Test Suite',
|
|
56
|
-
description: 'Collection of GraphConverter tests',
|
|
57
|
-
shape: 'rectangle',
|
|
58
|
-
fill: '#3b82f6',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
// Test Phase Nodes
|
|
63
|
-
{
|
|
64
|
-
id: 'setup-phase',
|
|
65
|
-
type: 'text',
|
|
66
|
-
text: 'Setup',
|
|
67
|
-
x: -250,
|
|
68
|
-
y: 50,
|
|
69
|
-
width: 120,
|
|
70
|
-
height: 80,
|
|
71
|
-
pv: {
|
|
72
|
-
nodeType: 'test-phase',
|
|
73
|
-
name: 'Setup Phase',
|
|
74
|
-
description: 'Test data preparation',
|
|
75
|
-
shape: 'hexagon',
|
|
76
|
-
fill: '#10b981',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: 'execution-phase',
|
|
81
|
-
type: 'text',
|
|
82
|
-
text: 'Execution',
|
|
83
|
-
x: -80,
|
|
84
|
-
y: 50,
|
|
85
|
-
width: 120,
|
|
86
|
-
height: 80,
|
|
87
|
-
pv: {
|
|
88
|
-
nodeType: 'test-phase',
|
|
89
|
-
name: 'Execution Phase',
|
|
90
|
-
description: 'Code under test runs',
|
|
91
|
-
shape: 'hexagon',
|
|
92
|
-
fill: '#f59e0b',
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'assertion-phase',
|
|
97
|
-
type: 'text',
|
|
98
|
-
text: 'Assertion',
|
|
99
|
-
x: 90,
|
|
100
|
-
y: 50,
|
|
101
|
-
width: 120,
|
|
102
|
-
height: 80,
|
|
103
|
-
pv: {
|
|
104
|
-
nodeType: 'test-phase',
|
|
105
|
-
name: 'Assertion Phase',
|
|
106
|
-
description: 'Verify results',
|
|
107
|
-
shape: 'hexagon',
|
|
108
|
-
fill: '#8b5cf6',
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
// Result Node
|
|
113
|
-
{
|
|
114
|
-
id: 'test-result',
|
|
115
|
-
type: 'text',
|
|
116
|
-
text: 'Test Result',
|
|
117
|
-
x: -100,
|
|
118
|
-
y: 200,
|
|
119
|
-
width: 240,
|
|
120
|
-
height: 80,
|
|
121
|
-
pv: {
|
|
122
|
-
nodeType: 'result',
|
|
123
|
-
name: 'Test Result',
|
|
124
|
-
description: 'Pass/Fail outcome',
|
|
125
|
-
shape: 'rectangle',
|
|
126
|
-
fill: '#10b981',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
edges: [
|
|
131
|
-
{
|
|
132
|
-
id: 'suite-to-setup',
|
|
133
|
-
fromNode: 'test-suite',
|
|
134
|
-
toNode: 'setup-phase',
|
|
135
|
-
fromSide: 'bottom',
|
|
136
|
-
toSide: 'top',
|
|
137
|
-
label: 'start test',
|
|
138
|
-
pv: {
|
|
139
|
-
edgeType: 'flow',
|
|
140
|
-
style: 'solid',
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: 'setup-to-execution',
|
|
145
|
-
fromNode: 'setup-phase',
|
|
146
|
-
toNode: 'execution-phase',
|
|
147
|
-
fromSide: 'right',
|
|
148
|
-
toSide: 'left',
|
|
149
|
-
label: 'data ready',
|
|
150
|
-
pv: {
|
|
151
|
-
edgeType: 'flow',
|
|
152
|
-
style: 'solid',
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: 'execution-to-assertion',
|
|
157
|
-
fromNode: 'execution-phase',
|
|
158
|
-
toNode: 'assertion-phase',
|
|
159
|
-
fromSide: 'right',
|
|
160
|
-
toSide: 'left',
|
|
161
|
-
label: 'got result',
|
|
162
|
-
pv: {
|
|
163
|
-
edgeType: 'flow',
|
|
164
|
-
style: 'solid',
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: 'assertion-to-result',
|
|
169
|
-
fromNode: 'assertion-phase',
|
|
170
|
-
toNode: 'test-result',
|
|
171
|
-
fromSide: 'bottom',
|
|
172
|
-
toSide: 'top',
|
|
173
|
-
label: 'complete',
|
|
174
|
-
pv: {
|
|
175
|
-
edgeType: 'flow',
|
|
176
|
-
style: 'solid',
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
pv: {
|
|
181
|
-
version: '1.0.0',
|
|
182
|
-
name: 'Test Execution Flow',
|
|
183
|
-
description: 'Visualizes the flow of test execution through phases',
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// Interactive Story (No Animation)
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
|
-
const AnimatedTestExecution = () => {
|
|
192
|
-
const [events] = useState<GraphEvent[]>([]);
|
|
193
|
-
const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
|
|
194
|
-
// Show all events by default - set to a large number
|
|
195
|
-
const [currentEventIndex] = useState(999);
|
|
196
|
-
const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
|
|
197
|
-
|
|
198
|
-
// Extract spans and logs from test data
|
|
199
|
-
const testData = testSpans as JsonObject;
|
|
200
|
-
const spans = Array.isArray(testData) ? testData : testData.spans || testData;
|
|
201
|
-
const logs = testData.logs || [];
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
|
|
205
|
-
{/* Event Panel - Left Side */}
|
|
206
|
-
<div style={{ flex: '0 0 50%', height: '100%', borderRight: `1px solid #333`, overflow: 'hidden' }}>
|
|
207
|
-
<TestEventPanel
|
|
208
|
-
spans={spans}
|
|
209
|
-
logs={logs}
|
|
210
|
-
currentSpanIndex={currentSpanIndex}
|
|
211
|
-
currentEventIndex={currentEventIndex}
|
|
212
|
-
highlightedPhase={highlightedPhase}
|
|
213
|
-
onSpanIndexChange={setCurrentSpanIndex}
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
{/* Graph Visualization - Right Side */}
|
|
218
|
-
<div
|
|
219
|
-
style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}
|
|
220
|
-
onMouseLeave={() => setHighlightedPhase(undefined)}
|
|
221
|
-
>
|
|
222
|
-
<div
|
|
223
|
-
style={{ width: '100%', height: '100%' }}
|
|
224
|
-
onMouseOver={(e) => {
|
|
225
|
-
// Check if hovering over a phase node
|
|
226
|
-
const target = e.target as HTMLElement;
|
|
227
|
-
const textContent = target.textContent;
|
|
228
|
-
if (textContent === 'Setup') setHighlightedPhase('setup');
|
|
229
|
-
else if (textContent === 'Execution') setHighlightedPhase('execution');
|
|
230
|
-
else if (textContent === 'Assertion') setHighlightedPhase('assertion');
|
|
231
|
-
}}
|
|
232
|
-
>
|
|
233
|
-
<GraphRenderer
|
|
234
|
-
canvas={testExecutionCanvas}
|
|
235
|
-
showControls={true}
|
|
236
|
-
events={events}
|
|
237
|
-
/>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
);
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Interactive visualization of real test execution data using the "wide event" pattern.
|
|
246
|
-
*
|
|
247
|
-
* This demonstrates the key concept from loggingsucks.com:
|
|
248
|
-
* - ONE comprehensive span per test (not multiple child spans)
|
|
249
|
-
* - Events show the narrative of what happened during execution
|
|
250
|
-
* - Context accumulates through event attributes with file/line information
|
|
251
|
-
* - Easy to search by test.name to get full execution story
|
|
252
|
-
*
|
|
253
|
-
* **Interaction:**
|
|
254
|
-
* - Hover over graph nodes (Setup, Execution, Assertion) to highlight related events
|
|
255
|
-
* - Watch the code journey: blue = test file, green = code under test
|
|
256
|
-
* - All events are shown immediately for easy review
|
|
257
|
-
*/
|
|
258
|
-
export const Animated: Story = {
|
|
259
|
-
render: () => <AnimatedTestExecution />,
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Static view of the test execution flow showing phases.
|
|
264
|
-
*/
|
|
265
|
-
export const StaticView: Story = {
|
|
266
|
-
args: {
|
|
267
|
-
canvas: testExecutionCanvas,
|
|
268
|
-
showControls: true,
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Event panel component showing test execution narrative with file/line information.
|
|
274
|
-
*
|
|
275
|
-
* Shows how events and logs are interleaved in chronological order, with automatic
|
|
276
|
-
* file/line capture from stack traces and severity-based color coding for logs.
|
|
277
|
-
*/
|
|
278
|
-
export const EventPanelOnly: StoryObj = {
|
|
279
|
-
render: () => {
|
|
280
|
-
const testData = testSpans as JsonObject;
|
|
281
|
-
const spans = Array.isArray(testData) ? testData : testData.spans || testData;
|
|
282
|
-
const logs = testData.logs || [];
|
|
283
|
-
|
|
284
|
-
return (
|
|
285
|
-
<div style={{ width: '600px', height: '100vh' }}>
|
|
286
|
-
<TestEventPanel
|
|
287
|
-
spans={spans}
|
|
288
|
-
logs={logs}
|
|
289
|
-
currentSpanIndex={0}
|
|
290
|
-
currentEventIndex={999} // Show all events
|
|
291
|
-
highlightedPhase={undefined}
|
|
292
|
-
/>
|
|
293
|
-
</div>
|
|
294
|
-
);
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Event panel with narrative view toggle
|
|
300
|
-
*
|
|
301
|
-
* Demonstrates the new narrative view mode that transforms raw OTEL events into
|
|
302
|
-
* human-readable execution narratives. Toggle between "Raw Events" and "Narrative"
|
|
303
|
-
* to see both views.
|
|
304
|
-
*
|
|
305
|
-
* Features:
|
|
306
|
-
* - Human-readable narrative generated from template
|
|
307
|
-
* - Scenario-based rendering (test-passed vs test-failed)
|
|
308
|
-
* - Syntax highlighting for better readability
|
|
309
|
-
* - Optional metadata display
|
|
310
|
-
*/
|
|
311
|
-
export const WithNarrativeToggle: StoryObj = {
|
|
312
|
-
render: () => {
|
|
313
|
-
const [viewMode, setViewMode] = useState<ViewMode>('narrative');
|
|
314
|
-
const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
|
|
315
|
-
const [showMetadata, setShowMetadata] = useState(false);
|
|
316
|
-
|
|
317
|
-
const testData = testSpans as JsonObject;
|
|
318
|
-
const spans = Array.isArray(testData) ? testData : testData.spans || testData;
|
|
319
|
-
const logs = testData.logs || [];
|
|
320
|
-
|
|
321
|
-
return (
|
|
322
|
-
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
323
|
-
{/* Controls */}
|
|
324
|
-
<div
|
|
325
|
-
style={{
|
|
326
|
-
padding: '12px 20px',
|
|
327
|
-
background: '#1a1a1a',
|
|
328
|
-
borderBottom: '1px solid #333',
|
|
329
|
-
display: 'flex',
|
|
330
|
-
gap: '12px',
|
|
331
|
-
alignItems: 'center',
|
|
332
|
-
}}
|
|
333
|
-
>
|
|
334
|
-
<label style={{ color: '#999', fontSize: '14px' }}>
|
|
335
|
-
<input
|
|
336
|
-
type="checkbox"
|
|
337
|
-
checked={showMetadata}
|
|
338
|
-
onChange={(e) => setShowMetadata(e.target.checked)}
|
|
339
|
-
style={{ marginRight: '6px' }}
|
|
340
|
-
/>
|
|
341
|
-
Show Metadata
|
|
342
|
-
</label>
|
|
343
|
-
</div>
|
|
344
|
-
|
|
345
|
-
{/* Event Panel */}
|
|
346
|
-
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
347
|
-
<TestEventPanel
|
|
348
|
-
spans={spans}
|
|
349
|
-
logs={logs}
|
|
350
|
-
currentSpanIndex={currentSpanIndex}
|
|
351
|
-
currentEventIndex={999}
|
|
352
|
-
onSpanIndexChange={setCurrentSpanIndex}
|
|
353
|
-
viewMode={viewMode}
|
|
354
|
-
narrativeTemplate={narrativeTemplate as NarrativeTemplate}
|
|
355
|
-
onViewModeChange={setViewMode}
|
|
356
|
-
showNarrativeMetadata={showMetadata}
|
|
357
|
-
/>
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
);
|
|
361
|
-
},
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Split view with narrative on left, graph on right
|
|
366
|
-
*
|
|
367
|
-
* Shows how the narrative view integrates with the graph visualization,
|
|
368
|
-
* providing a complete picture of test execution.
|
|
369
|
-
*/
|
|
370
|
-
export const NarrativeWithGraph: StoryObj = {
|
|
371
|
-
render: () => {
|
|
372
|
-
const [viewMode, setViewMode] = useState<ViewMode>('narrative');
|
|
373
|
-
const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
|
|
374
|
-
const [events] = useState<GraphEvent[]>([]);
|
|
375
|
-
|
|
376
|
-
const testData = testSpans as JsonObject;
|
|
377
|
-
const spans = Array.isArray(testData) ? testData : testData.spans || testData;
|
|
378
|
-
const logs = testData.logs || [];
|
|
379
|
-
|
|
380
|
-
return (
|
|
381
|
-
<div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
|
|
382
|
-
{/* Event Panel - Left Side */}
|
|
383
|
-
<div style={{ flex: '0 0 50%', height: '100%', borderRight: '1px solid #333', overflow: 'hidden' }}>
|
|
384
|
-
<TestEventPanel
|
|
385
|
-
spans={spans}
|
|
386
|
-
logs={logs}
|
|
387
|
-
currentSpanIndex={currentSpanIndex}
|
|
388
|
-
currentEventIndex={999}
|
|
389
|
-
onSpanIndexChange={setCurrentSpanIndex}
|
|
390
|
-
viewMode={viewMode}
|
|
391
|
-
narrativeTemplate={narrativeTemplate as NarrativeTemplate}
|
|
392
|
-
onViewModeChange={setViewMode}
|
|
393
|
-
showNarrativeMetadata={true}
|
|
394
|
-
/>
|
|
395
|
-
</div>
|
|
396
|
-
|
|
397
|
-
{/* Graph Visualization - Right Side */}
|
|
398
|
-
<div style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}>
|
|
399
|
-
<GraphRenderer canvas={testExecutionCanvas} showControls={true} events={events} />
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
);
|
|
403
|
-
},
|
|
404
|
-
};
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import { GraphRenderer } from '../components/GraphRenderer';
|
|
4
|
-
import { TestEventPanel } from '../components/TestEventPanel';
|
|
5
|
-
import type { ExtendedCanvas, JsonValue } from '@principal-ai/principal-view-core/browser';
|
|
6
|
-
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
7
|
-
import executionCanvas from '../../../../.principal-views/graph-converter-execution.otel.canvas';
|
|
8
|
-
import validatedSpans from './data/graph-converter-validated-execution.json';
|
|
9
|
-
|
|
10
|
-
const meta = {
|
|
11
|
-
title: 'Features/Validated Execution',
|
|
12
|
-
component: GraphRenderer,
|
|
13
|
-
parameters: {
|
|
14
|
-
layout: 'fullscreen',
|
|
15
|
-
docs: {
|
|
16
|
-
description: {
|
|
17
|
-
component:
|
|
18
|
-
'Demonstrates type-safe event emission with schema validation. The canvas defines expected events, and production code is validated against this schema. Shows how events match the schema defined in graph-converter-execution.otel.canvas.',
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
tags: ['autodocs'],
|
|
23
|
-
decorators: [
|
|
24
|
-
(Story) => (
|
|
25
|
-
<ThemeProvider theme={defaultEditorTheme}>
|
|
26
|
-
<div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
|
|
27
|
-
<Story />
|
|
28
|
-
</div>
|
|
29
|
-
</ThemeProvider>
|
|
30
|
-
),
|
|
31
|
-
],
|
|
32
|
-
} satisfies Meta<typeof GraphRenderer>;
|
|
33
|
-
|
|
34
|
-
export default meta;
|
|
35
|
-
type Story = StoryObj<typeof meta>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Graph visualization of the execution flow with event schema definitions.
|
|
39
|
-
*
|
|
40
|
-
* This canvas defines:
|
|
41
|
-
* - `graph-converter` node with 5 event types
|
|
42
|
-
* - `validation` node with 2 event types
|
|
43
|
-
* - `graph-output` node with 1 event type
|
|
44
|
-
*
|
|
45
|
-
* Each event type has a schema defining:
|
|
46
|
-
* - Required/optional fields
|
|
47
|
-
* - Field types (string, number, boolean, etc.)
|
|
48
|
-
* - Field descriptions
|
|
49
|
-
*
|
|
50
|
-
* See `.principal-views/graph-converter-execution.otel.canvas` for the full schema.
|
|
51
|
-
*/
|
|
52
|
-
export const ExecutionFlow: Story = {
|
|
53
|
-
args: {
|
|
54
|
-
canvas: executionCanvas as ExtendedCanvas,
|
|
55
|
-
showControls: true,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Event panel showing validated execution data.
|
|
61
|
-
*
|
|
62
|
-
* These events were emitted using `createValidatedSpanEmitter()` which:
|
|
63
|
-
* - Validates events against the canvas schema
|
|
64
|
-
* - Ensures required fields are present
|
|
65
|
-
* - Checks field types match the schema
|
|
66
|
-
* - Throws errors in strict mode if validation fails
|
|
67
|
-
*
|
|
68
|
-
* All events in this panel passed schema validation.
|
|
69
|
-
*/
|
|
70
|
-
export const ValidatedEvents: Story = {
|
|
71
|
-
render: () => (
|
|
72
|
-
<div style={{ width: '800px', height: '100vh' }}>
|
|
73
|
-
<TestEventPanel
|
|
74
|
-
spans={validatedSpans as JsonValue[]}
|
|
75
|
-
currentSpanIndex={0}
|
|
76
|
-
currentEventIndex={10} // Show all events
|
|
77
|
-
highlightedPhase={undefined}
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
),
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Side-by-side view of execution flow and validated events.
|
|
85
|
-
*
|
|
86
|
-
* **How it works:**
|
|
87
|
-
* 1. Canvas defines event schemas (what events should be emitted)
|
|
88
|
-
* 2. Tests use `createValidatedSpanEmitter()` to emit events
|
|
89
|
-
* 3. Events are validated against the schema in strict mode
|
|
90
|
-
* 4. If validation fails, test throws `EventValidationError`
|
|
91
|
-
* 5. If validation passes, events are emitted and collected
|
|
92
|
-
*
|
|
93
|
-
* This ensures production code emits events that match the architecture.
|
|
94
|
-
*/
|
|
95
|
-
export const FlowWithValidation: Story = {
|
|
96
|
-
render: () => (
|
|
97
|
-
<div style={{ display: 'flex', width: '100%', height: '100%' }}>
|
|
98
|
-
{/* Graph Visualization - Left Side */}
|
|
99
|
-
<div style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}>
|
|
100
|
-
<GraphRenderer
|
|
101
|
-
canvas={executionCanvas as ExtendedCanvas}
|
|
102
|
-
showControls={true}
|
|
103
|
-
/>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
{/* Event Panel - Right Side */}
|
|
107
|
-
<div style={{ flex: '0 0 40%', height: '100%', borderLeft: '1px solid #333' }}>
|
|
108
|
-
<div style={{ padding: '20px', color: '#fff', borderBottom: '1px solid #333' }}>
|
|
109
|
-
<h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>
|
|
110
|
-
Type-Safe Validated Events
|
|
111
|
-
</h3>
|
|
112
|
-
<p style={{ margin: 0, fontSize: '12px', color: '#888' }}>
|
|
113
|
-
Events validated against canvas schema. See{' '}
|
|
114
|
-
<code>.principal-views/graph-converter-execution.otel.canvas</code>
|
|
115
|
-
</p>
|
|
116
|
-
</div>
|
|
117
|
-
<TestEventPanel
|
|
118
|
-
spans={validatedSpans as JsonValue[]}
|
|
119
|
-
currentSpanIndex={0}
|
|
120
|
-
currentEventIndex={10}
|
|
121
|
-
highlightedPhase={undefined}
|
|
122
|
-
/>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
),
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Canvas with event schema definitions (JSON view).
|
|
130
|
-
*
|
|
131
|
-
* Shows the raw canvas structure including event schemas.
|
|
132
|
-
* Notice the `pv.events` property on each node defining:
|
|
133
|
-
* - Event names (e.g., "conversion.started")
|
|
134
|
-
* - Event descriptions
|
|
135
|
-
* - Field schemas with types and requirements
|
|
136
|
-
*/
|
|
137
|
-
export const CanvasSchema: Story = {
|
|
138
|
-
render: () => (
|
|
139
|
-
<div
|
|
140
|
-
style={{
|
|
141
|
-
padding: '20px',
|
|
142
|
-
color: '#fff',
|
|
143
|
-
fontFamily: 'monospace',
|
|
144
|
-
fontSize: '12px',
|
|
145
|
-
overflow: 'auto',
|
|
146
|
-
height: '100vh',
|
|
147
|
-
}}
|
|
148
|
-
>
|
|
149
|
-
<h2>Event Schema Definition</h2>
|
|
150
|
-
<p>
|
|
151
|
-
This canvas defines event schemas for type-safe telemetry validation.
|
|
152
|
-
</p>
|
|
153
|
-
<pre style={{ background: '#1e1e1e', padding: '20px', borderRadius: '8px' }}>
|
|
154
|
-
{JSON.stringify(executionCanvas, null, 2)}
|
|
155
|
-
</pre>
|
|
156
|
-
</div>
|
|
157
|
-
),
|
|
158
|
-
};
|