@principal-ai/principal-view-react 0.13.9 → 0.13.11
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/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +39 -8
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/MultiCanvasRenderer.d.ts +75 -0
- package/dist/components/MultiCanvasRenderer.d.ts.map +1 -0
- package/dist/components/MultiCanvasRenderer.js +142 -0
- package/dist/components/MultiCanvasRenderer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/canvasBounds.d.ts +29 -0
- package/dist/utils/canvasBounds.d.ts.map +1 -0
- package/dist/utils/canvasBounds.js +56 -0
- package/dist/utils/canvasBounds.js.map +1 -0
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +46 -9
- package/src/components/MultiCanvasRenderer.tsx +225 -0
- package/src/index.ts +9 -0
- package/src/stories/MultiCanvasRenderer.stories.tsx +329 -0
- package/src/utils/canvasBounds.ts +91 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { MultiCanvasRenderer } from '../components/MultiCanvasRenderer';
|
|
4
|
+
import type { MultiCanvasLayout } from '../components/MultiCanvasRenderer';
|
|
5
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
6
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/MultiCanvasRenderer',
|
|
10
|
+
component: MultiCanvasRenderer,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullscreen',
|
|
13
|
+
},
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
18
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
} satisfies Meta<typeof MultiCanvasRenderer>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Sample Canvases
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const authFlowCanvas: ExtendedCanvas = {
|
|
34
|
+
nodes: [
|
|
35
|
+
{
|
|
36
|
+
id: 'login',
|
|
37
|
+
type: 'text',
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
width: 120,
|
|
41
|
+
height: 60,
|
|
42
|
+
text: 'Login',
|
|
43
|
+
color: '#4A90E2',
|
|
44
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'User' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'validate',
|
|
48
|
+
type: 'text',
|
|
49
|
+
x: 170,
|
|
50
|
+
y: 0,
|
|
51
|
+
width: 120,
|
|
52
|
+
height: 60,
|
|
53
|
+
text: 'Validate',
|
|
54
|
+
color: '#F5A623',
|
|
55
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'Shield' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'session',
|
|
59
|
+
type: 'text',
|
|
60
|
+
x: 340,
|
|
61
|
+
y: 0,
|
|
62
|
+
width: 120,
|
|
63
|
+
height: 60,
|
|
64
|
+
text: 'Session',
|
|
65
|
+
color: '#7ED321',
|
|
66
|
+
pv: { nodeType: 'data', shape: 'rectangle', icon: 'Key' },
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
edges: [
|
|
70
|
+
{ id: 'e1', fromNode: 'login', toNode: 'validate', pv: { edgeType: 'flow' } },
|
|
71
|
+
{ id: 'e2', fromNode: 'validate', toNode: 'session', pv: { edgeType: 'flow' } },
|
|
72
|
+
],
|
|
73
|
+
pv: {
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
name: 'Authentication Flow',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const dataProcessingCanvas: ExtendedCanvas = {
|
|
80
|
+
nodes: [
|
|
81
|
+
{
|
|
82
|
+
id: 'input',
|
|
83
|
+
type: 'text',
|
|
84
|
+
x: 0,
|
|
85
|
+
y: 0,
|
|
86
|
+
width: 100,
|
|
87
|
+
height: 100,
|
|
88
|
+
text: 'Input',
|
|
89
|
+
color: '#7B68EE',
|
|
90
|
+
pv: { nodeType: 'data', shape: 'circle', icon: 'Download' },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'transform',
|
|
94
|
+
type: 'text',
|
|
95
|
+
x: 150,
|
|
96
|
+
y: 15,
|
|
97
|
+
width: 140,
|
|
98
|
+
height: 70,
|
|
99
|
+
text: 'Transform',
|
|
100
|
+
color: '#4A90E2',
|
|
101
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'Settings' },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'output',
|
|
105
|
+
type: 'text',
|
|
106
|
+
x: 340,
|
|
107
|
+
y: 0,
|
|
108
|
+
width: 100,
|
|
109
|
+
height: 100,
|
|
110
|
+
text: 'Output',
|
|
111
|
+
color: '#7B68EE',
|
|
112
|
+
pv: { nodeType: 'data', shape: 'circle', icon: 'Upload' },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
edges: [
|
|
116
|
+
{ id: 'e1', fromNode: 'input', toNode: 'transform', pv: { edgeType: 'dataflow' } },
|
|
117
|
+
{ id: 'e2', fromNode: 'transform', toNode: 'output', pv: { edgeType: 'dataflow' } },
|
|
118
|
+
],
|
|
119
|
+
pv: {
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
name: 'Data Processing Pipeline',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const notificationCanvas: ExtendedCanvas = {
|
|
126
|
+
nodes: [
|
|
127
|
+
{
|
|
128
|
+
id: 'event',
|
|
129
|
+
type: 'text',
|
|
130
|
+
x: 0,
|
|
131
|
+
y: 30,
|
|
132
|
+
width: 100,
|
|
133
|
+
height: 60,
|
|
134
|
+
text: 'Event',
|
|
135
|
+
color: '#F5A623',
|
|
136
|
+
pv: { nodeType: 'trigger', shape: 'rectangle', icon: 'Zap' },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'router',
|
|
140
|
+
type: 'text',
|
|
141
|
+
x: 150,
|
|
142
|
+
y: 0,
|
|
143
|
+
width: 100,
|
|
144
|
+
height: 100,
|
|
145
|
+
text: 'Router',
|
|
146
|
+
color: '#4A90E2',
|
|
147
|
+
pv: { nodeType: 'process', shape: 'diamond', icon: 'GitBranch' },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'email',
|
|
151
|
+
type: 'text',
|
|
152
|
+
x: 300,
|
|
153
|
+
y: -30,
|
|
154
|
+
width: 100,
|
|
155
|
+
height: 50,
|
|
156
|
+
text: 'Email',
|
|
157
|
+
color: '#7ED321',
|
|
158
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'Mail' },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'sms',
|
|
162
|
+
type: 'text',
|
|
163
|
+
x: 300,
|
|
164
|
+
y: 40,
|
|
165
|
+
width: 100,
|
|
166
|
+
height: 50,
|
|
167
|
+
text: 'SMS',
|
|
168
|
+
color: '#7ED321',
|
|
169
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'MessageSquare' },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'push',
|
|
173
|
+
type: 'text',
|
|
174
|
+
x: 300,
|
|
175
|
+
y: 110,
|
|
176
|
+
width: 100,
|
|
177
|
+
height: 50,
|
|
178
|
+
text: 'Push',
|
|
179
|
+
color: '#7ED321',
|
|
180
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'Bell' },
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
edges: [
|
|
184
|
+
{ id: 'e1', fromNode: 'event', toNode: 'router', pv: { edgeType: 'flow' } },
|
|
185
|
+
{ id: 'e2', fromNode: 'router', toNode: 'email', pv: { edgeType: 'flow' } },
|
|
186
|
+
{ id: 'e3', fromNode: 'router', toNode: 'sms', pv: { edgeType: 'flow' } },
|
|
187
|
+
{ id: 'e4', fromNode: 'router', toNode: 'push', pv: { edgeType: 'flow' } },
|
|
188
|
+
],
|
|
189
|
+
pv: {
|
|
190
|
+
version: '1.0.0',
|
|
191
|
+
name: 'Notification System',
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const monitoringCanvas: ExtendedCanvas = {
|
|
196
|
+
nodes: [
|
|
197
|
+
{
|
|
198
|
+
id: 'metrics',
|
|
199
|
+
type: 'text',
|
|
200
|
+
x: 0,
|
|
201
|
+
y: 0,
|
|
202
|
+
width: 100,
|
|
203
|
+
height: 60,
|
|
204
|
+
text: 'Metrics',
|
|
205
|
+
color: '#4A90E2',
|
|
206
|
+
pv: { nodeType: 'data', shape: 'rectangle', icon: 'BarChart2' },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'alert',
|
|
210
|
+
type: 'text',
|
|
211
|
+
x: 150,
|
|
212
|
+
y: 0,
|
|
213
|
+
width: 100,
|
|
214
|
+
height: 60,
|
|
215
|
+
text: 'Alert',
|
|
216
|
+
color: '#D0021B',
|
|
217
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'AlertTriangle' },
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
edges: [
|
|
221
|
+
{ id: 'e1', fromNode: 'metrics', toNode: 'alert', pv: { edgeType: 'flow' } },
|
|
222
|
+
],
|
|
223
|
+
pv: {
|
|
224
|
+
version: '1.0.0',
|
|
225
|
+
name: 'Monitoring',
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// Stories
|
|
231
|
+
// ============================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Two canvases side by side on the same graph
|
|
235
|
+
*/
|
|
236
|
+
const twoCanvasLayout: MultiCanvasLayout = {
|
|
237
|
+
placements: [
|
|
238
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
239
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const TwoCanvasesSideBySide: Story = {
|
|
244
|
+
args: {
|
|
245
|
+
layout: twoCanvasLayout,
|
|
246
|
+
showControls: true,
|
|
247
|
+
showBackground: true,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Four canvases arranged in a 2x2 grid pattern
|
|
253
|
+
*/
|
|
254
|
+
const fourCanvasLayout: MultiCanvasLayout = {
|
|
255
|
+
placements: [
|
|
256
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
257
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
|
|
258
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 200 } },
|
|
259
|
+
{ canvasId: 'monitor', canvas: monitoringCanvas, position: { x: 600, y: 200 } },
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const FourCanvasesGrid: Story = {
|
|
264
|
+
args: {
|
|
265
|
+
layout: fourCanvasLayout,
|
|
266
|
+
showControls: true,
|
|
267
|
+
showBackground: true,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Canvases stacked vertically
|
|
273
|
+
*/
|
|
274
|
+
const verticalLayout: MultiCanvasLayout = {
|
|
275
|
+
placements: [
|
|
276
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
277
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 0, y: 150 } },
|
|
278
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 300 } },
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export const VerticalStack: Story = {
|
|
283
|
+
args: {
|
|
284
|
+
layout: verticalLayout,
|
|
285
|
+
showControls: true,
|
|
286
|
+
showBackground: true,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Single canvas (baseline comparison)
|
|
292
|
+
*/
|
|
293
|
+
const singleCanvasLayout: MultiCanvasLayout = {
|
|
294
|
+
placements: [
|
|
295
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 0 } },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const SingleCanvas: Story = {
|
|
300
|
+
args: {
|
|
301
|
+
layout: singleCanvasLayout,
|
|
302
|
+
showControls: true,
|
|
303
|
+
showBackground: true,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* With minimap enabled to navigate the combined view
|
|
309
|
+
*/
|
|
310
|
+
export const WithMinimap: Story = {
|
|
311
|
+
args: {
|
|
312
|
+
layout: fourCanvasLayout,
|
|
313
|
+
showControls: true,
|
|
314
|
+
showMinimap: true,
|
|
315
|
+
showBackground: true,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Without group borders - just the nodes merged together
|
|
321
|
+
*/
|
|
322
|
+
export const WithoutGroups: Story = {
|
|
323
|
+
args: {
|
|
324
|
+
layout: fourCanvasLayout,
|
|
325
|
+
showControls: true,
|
|
326
|
+
showBackground: true,
|
|
327
|
+
showGroups: false,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
2
|
+
|
|
3
|
+
export interface CanvasBounds {
|
|
4
|
+
minX: number;
|
|
5
|
+
minY: number;
|
|
6
|
+
maxX: number;
|
|
7
|
+
maxY: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
13
|
+
const DEFAULT_NODE_HEIGHT = 100;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate the bounding box of all nodes in a canvas.
|
|
17
|
+
* Returns the min/max coordinates and total dimensions.
|
|
18
|
+
*/
|
|
19
|
+
export function getCanvasBounds(canvas: ExtendedCanvas): CanvasBounds {
|
|
20
|
+
if (!canvas.nodes || canvas.nodes.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
minX: 0,
|
|
23
|
+
minY: 0,
|
|
24
|
+
maxX: DEFAULT_NODE_WIDTH,
|
|
25
|
+
maxY: DEFAULT_NODE_HEIGHT,
|
|
26
|
+
width: DEFAULT_NODE_WIDTH,
|
|
27
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let minX = Infinity;
|
|
32
|
+
let minY = Infinity;
|
|
33
|
+
let maxX = -Infinity;
|
|
34
|
+
let maxY = -Infinity;
|
|
35
|
+
|
|
36
|
+
for (const node of canvas.nodes) {
|
|
37
|
+
const x = node.x ?? 0;
|
|
38
|
+
const y = node.y ?? 0;
|
|
39
|
+
const width = node.width ?? DEFAULT_NODE_WIDTH;
|
|
40
|
+
const height = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
41
|
+
|
|
42
|
+
minX = Math.min(minX, x);
|
|
43
|
+
minY = Math.min(minY, y);
|
|
44
|
+
maxX = Math.max(maxX, x + width);
|
|
45
|
+
maxY = Math.max(maxY, y + height);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
minX,
|
|
50
|
+
minY,
|
|
51
|
+
maxX,
|
|
52
|
+
maxY,
|
|
53
|
+
width: maxX - minX,
|
|
54
|
+
height: maxY - minY,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Calculate a recommended display size for a canvas cell.
|
|
60
|
+
* Adds padding and enforces minimum dimensions.
|
|
61
|
+
*/
|
|
62
|
+
export function getCanvasDisplaySize(
|
|
63
|
+
canvas: ExtendedCanvas,
|
|
64
|
+
options: {
|
|
65
|
+
padding?: number;
|
|
66
|
+
minWidth?: number;
|
|
67
|
+
minHeight?: number;
|
|
68
|
+
maxWidth?: number;
|
|
69
|
+
maxHeight?: number;
|
|
70
|
+
} = {}
|
|
71
|
+
): { width: number; height: number } {
|
|
72
|
+
const {
|
|
73
|
+
padding = 100,
|
|
74
|
+
minWidth = 300,
|
|
75
|
+
minHeight = 200,
|
|
76
|
+
maxWidth = 1200,
|
|
77
|
+
maxHeight = 800,
|
|
78
|
+
} = options;
|
|
79
|
+
|
|
80
|
+
const bounds = getCanvasBounds(canvas);
|
|
81
|
+
|
|
82
|
+
// Add padding to content dimensions
|
|
83
|
+
const contentWidth = bounds.width + padding * 2;
|
|
84
|
+
const contentHeight = bounds.height + padding * 2;
|
|
85
|
+
|
|
86
|
+
// Clamp to min/max
|
|
87
|
+
const width = Math.min(maxWidth, Math.max(minWidth, contentWidth));
|
|
88
|
+
const height = Math.min(maxHeight, Math.max(minHeight, contentHeight));
|
|
89
|
+
|
|
90
|
+
return { width, height };
|
|
91
|
+
}
|