@live-change/flow-frontend 0.9.226 → 0.9.228
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/.node-version +1 -0
- package/.nvmrc +1 -0
- package/README.md +33 -0
- package/e2e/calculation.test.ts +247 -0
- package/e2e/e2eSuite.ts +8 -0
- package/e2e/env.ts +86 -0
- package/e2e/kitchen-sink.test.ts +85 -0
- package/e2e/runner.ts +10 -0
- package/e2e/withBrowser.ts +5 -0
- package/front/components.d.ts +25 -0
- package/front/locales/en.js +27 -0
- package/front/locales/en.json +5 -0
- package/front/src/App.vue +38 -12
- package/front/src/NavBar.vue +31 -0
- package/front/src/components/Edge.vue +1 -1
- package/front/src/components/EdgeEndHandle.vue +1 -1
- package/front/src/components/Flow.vue +5 -4
- package/front/src/components/Node.vue +1 -1
- package/front/src/components/NodeHandle.vue +1 -1
- package/front/src/components/NodePort.vue +1 -2
- package/front/src/components/index.js +10 -10
- package/front/src/config.js +90 -0
- package/front/src/demo/DemoEdge.vue +38 -0
- package/front/src/demo/DemoNodeCard.vue +86 -0
- package/front/src/demo/DemoPort.vue +34 -0
- package/front/src/demo/EnrichNode.vue +37 -0
- package/front/src/demo/FlowKitchenSink.vue +165 -0
- package/front/src/demo/OutputNode.vue +45 -0
- package/front/src/demo/SourceNode.vue +49 -0
- package/front/src/demo/calculation/AddNode.vue +35 -0
- package/front/src/demo/calculation/ConstantNode.vue +61 -0
- package/front/src/demo/calculation/FlowCalculationDemo.vue +300 -0
- package/front/src/demo/calculation/MultiplyNode.vue +35 -0
- package/front/src/demo/calculation/PowerNode.vue +62 -0
- package/front/src/demo/flowTestId.js +3 -0
- package/front/src/demo/runDemoGraph.js +76 -0
- package/front/src/demo/seedGraph.js +38 -0
- package/front/src/entry-client.js +2 -2
- package/front/src/entry-server.js +2 -1
- package/front/src/router.js +20 -12
- package/package.json +44 -37
- package/server/app.config.js +61 -0
- package/server/calculation/calculation.js +20 -0
- package/server/calculation/config.js +15 -0
- package/server/calculation/definition.js +11 -0
- package/server/calculation/index.js +12 -0
- package/server/calculation/ops.js +69 -0
- package/server/calculation/run.js +69 -0
- package/server/calculation/runGraph.js +85 -0
- package/server/calculation/tests/run.test.js +73 -0
- package/server/calculation/triggers.js +95 -0
- package/server/calculation/wire.js +30 -0
- package/server/init.js +1 -6
- package/server/services.list.js +11 -0
- package/server/start.js +39 -0
- package/server/services.config.js +0 -26
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
transform, transformOrigin: '0 0',
|
|
16
16
|
width: flow?.size?.width + 'px', height: flow?.size?.height + 'px'
|
|
17
17
|
}">
|
|
18
|
-
<!-- <pre class="absolute text-xs">{{ JSON.stringify(freeEdge, null, 2) }}</pre>-->
|
|
19
18
|
<svg xmlns="http://www.w3.org/2000/svg" class="absolute w-full h-full pointer-events-none">
|
|
20
19
|
<slot name="background-edges" v-bind="flow" />
|
|
21
20
|
</svg>
|
|
22
|
-
<slot v-bind="flow" />
|
|
23
21
|
<svg xmlns="http://www.w3.org/2000/svg" class="absolute w-full h-full pointer-events-none">
|
|
24
22
|
<slot name="foreground-edges" v-bind="flow" />
|
|
23
|
+
</svg>
|
|
24
|
+
<slot v-bind="flow" />
|
|
25
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="absolute w-full h-full pointer-events-none">
|
|
25
26
|
<slot name="free-edge" v-bind="{ freeEdge, flow }" />
|
|
26
27
|
</svg>
|
|
27
28
|
</div>
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
|
|
31
32
|
<script setup>
|
|
32
33
|
|
|
33
|
-
import { defineProps, defineEmits, toRefs, reactive, ref, onMounted, onUnmounted, watch, computed } from "vue"
|
|
34
|
+
import { defineProps, defineEmits, toRefs, reactive, ref, onMounted, onUnmounted, watch, computed, unref } from "vue"
|
|
34
35
|
import { useFlow } from "./index.js"
|
|
35
36
|
|
|
36
37
|
const viewport = ref(null)
|
|
@@ -127,7 +128,7 @@
|
|
|
127
128
|
if(flow.options.connect) {
|
|
128
129
|
flow.options.connect(newEdge)
|
|
129
130
|
} else {
|
|
130
|
-
flow.edges.push(newEdge)
|
|
131
|
+
unref(flow.edges).push(newEdge)
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
},
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
const { node, portId, direction, newEdgeOptions } = toRefs(props)
|
|
44
44
|
|
|
45
|
-
const flow = inject("flow")
|
|
45
|
+
const flow = inject("flow", null)
|
|
46
46
|
|
|
47
47
|
const element = ref(null)
|
|
48
48
|
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
|
|
57
57
|
function handleMouseDown(event) {
|
|
58
58
|
event.preventDefault()
|
|
59
|
-
console.log("start draw edge", node.value.id, portId.value)
|
|
60
59
|
flow.startDrawEdge(node.value, portId.value, event, newEdgeOptions)
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ref, unref, reactive, computed, inject, provide, watch } from "vue"
|
|
2
2
|
|
|
3
3
|
export function useFlow(options) {
|
|
4
|
-
const existingFlow = inject("flow")
|
|
4
|
+
const existingFlow = inject("flow", null)
|
|
5
5
|
if(existingFlow && options) throw new Error("Flow already exists")
|
|
6
6
|
if(existingFlow) return existingFlow
|
|
7
7
|
|
|
@@ -98,14 +98,15 @@ export function useFlow(options) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function findNode(nodeId) {
|
|
101
|
+
nodeId = getNodeId(nodeId)
|
|
101
102
|
if(options.findNode) return options.findNode(nodeId)
|
|
102
103
|
if(options.findNodeIndex) return unref(nodes)[options.findNodeIndex(nodeId)]
|
|
103
|
-
return unref(nodes).
|
|
104
|
+
return unref(nodes).find(n => n.id == nodeId)
|
|
104
105
|
}
|
|
105
106
|
function findNodeIndex(nodeId) {
|
|
106
107
|
nodeId = getNodeId(nodeId)
|
|
107
108
|
if(options.findNodeIndex) return options.findNodeIndex(nodeId)
|
|
108
|
-
return unref(nodes).
|
|
109
|
+
return unref(nodes).findIndex(n => n.id == nodeId)
|
|
109
110
|
}
|
|
110
111
|
function getNodeId(nodeId) {
|
|
111
112
|
if(typeof nodeId == "string") return nodeId
|
|
@@ -118,14 +119,15 @@ export function useFlow(options) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
function findEdge(edgeId) {
|
|
122
|
+
edgeId = getEdgeId(edgeId)
|
|
121
123
|
if(options.findEdge) return options.findEdge(edgeId)
|
|
122
124
|
if(options.findEdgeIndex) return unref(edges)[options.findEdgeIndex(edgeId)]
|
|
123
|
-
return unref(edges).
|
|
125
|
+
return unref(edges).find(e => e.id == edgeId)
|
|
124
126
|
}
|
|
125
127
|
function findEdgeIndex(edgeId) {
|
|
126
128
|
edgeId = getEdgeId(edgeId)
|
|
127
129
|
if(options.findEdgeIndex) return options.findEdgeIndex(edgeId)
|
|
128
|
-
return unref(edges).
|
|
130
|
+
return unref(edges).findIndex(e => e.id == edgeId)
|
|
129
131
|
}
|
|
130
132
|
function getEdgeId(edgeId) {
|
|
131
133
|
if(typeof edgeId == "string") return edgeId
|
|
@@ -160,7 +162,6 @@ export function useFlow(options) {
|
|
|
160
162
|
function setPortElement(nodeId, portId, element, direction) {
|
|
161
163
|
nodeId = getNodeId(nodeId)
|
|
162
164
|
if (typeof portId != "string") portId = portId.id ?? `${nodeId}/${portId.name}`
|
|
163
|
-
console.log("setPortElement", nodeId, portId, element)
|
|
164
165
|
const nodeView = nodeViews.get(nodeId)
|
|
165
166
|
if(!nodeView) {
|
|
166
167
|
return
|
|
@@ -265,9 +266,8 @@ export function useFlow(options) {
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
function startDrawEdge(nodeId, portId, event, newEdgeOptions) {
|
|
268
|
-
console.log("startDrawEdge", nodeId, portId, event)
|
|
269
269
|
nodeId = getNodeId(nodeId)
|
|
270
|
-
if (typeof portId != "string") portId = portId.id ?? `${
|
|
270
|
+
if (typeof portId != "string") portId = portId.id ?? `${nodeId}/${portId.name}`
|
|
271
271
|
dragState.value = {
|
|
272
272
|
type: "drawEdge",
|
|
273
273
|
node: nodeId,
|
|
@@ -283,7 +283,7 @@ export function useFlow(options) {
|
|
|
283
283
|
if(options.deleteEdge) {
|
|
284
284
|
options.deleteEdge(unref(edges)[edgeIndex])
|
|
285
285
|
} else {
|
|
286
|
-
edges.splice(edgeIndex, 1)
|
|
286
|
+
unref(edges).splice(edgeIndex, 1)
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
@@ -298,7 +298,7 @@ export function useFlow(options) {
|
|
|
298
298
|
if(options.deleteNode) {
|
|
299
299
|
options.deleteNode(unref(nodes)[nodeIndex])
|
|
300
300
|
} else {
|
|
301
|
-
nodes.splice(nodeIndex, 1)
|
|
301
|
+
unref(nodes).splice(nodeIndex, 1)
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import deepmerge from 'deepmerge'
|
|
2
|
+
|
|
3
|
+
import * as en from '../locales/en.js'
|
|
4
|
+
import flowEn from '../locales/en.json'
|
|
5
|
+
import { locales as baseLocales } from '@live-change/frontend-base'
|
|
6
|
+
|
|
7
|
+
import Aura from '@primeuix/themes/aura'
|
|
8
|
+
import { definePreset } from '@primeuix/themes'
|
|
9
|
+
|
|
10
|
+
const levels = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
|
|
14
|
+
primeVue: {
|
|
15
|
+
theme: {
|
|
16
|
+
preset: definePreset(Aura, {
|
|
17
|
+
semantic: {
|
|
18
|
+
primary: Object.fromEntries(
|
|
19
|
+
levels.map(level => [level, `{blue.${level}}`])
|
|
20
|
+
),
|
|
21
|
+
colorScheme: {
|
|
22
|
+
light: {
|
|
23
|
+
surface: {
|
|
24
|
+
0: '#ffffff',
|
|
25
|
+
...Object.fromEntries(
|
|
26
|
+
levels.map(level => [level, `{zinc.${level}}`])
|
|
27
|
+
)
|
|
28
|
+
},
|
|
29
|
+
primary: {
|
|
30
|
+
contrastColor: '#000000'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
dark: {
|
|
34
|
+
surface: {
|
|
35
|
+
0: '#ffffff',
|
|
36
|
+
...Object.fromEntries(
|
|
37
|
+
levels.map(level => [level, `{slate.${level}}`])
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
options: {
|
|
45
|
+
prefix: 'p',
|
|
46
|
+
darkModeSelector: '.app-dark-mode',
|
|
47
|
+
cssLayer: {
|
|
48
|
+
name: 'primevue',
|
|
49
|
+
order: 'base, primevue'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
ripple: true
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
defaultLocale: 'en',
|
|
57
|
+
localeSelector: ({ headers }) => {
|
|
58
|
+
const acceptLanguage = headers?.['accept-language']
|
|
59
|
+
if (acceptLanguage) {
|
|
60
|
+
const code = acceptLanguage.split(',')[0].trim().split('-')[0]
|
|
61
|
+
if (code) return code
|
|
62
|
+
}
|
|
63
|
+
if (typeof window !== 'undefined') {
|
|
64
|
+
const html = document.querySelector('html')
|
|
65
|
+
const lang = html?.getAttribute('lang')
|
|
66
|
+
if (lang) return lang.split('-')[0]
|
|
67
|
+
}
|
|
68
|
+
return 'en'
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
i18n: {
|
|
72
|
+
messages: {
|
|
73
|
+
en: deepmerge.all([
|
|
74
|
+
baseLocales.en,
|
|
75
|
+
flowEn,
|
|
76
|
+
en.messages
|
|
77
|
+
])
|
|
78
|
+
},
|
|
79
|
+
numberFormats: {
|
|
80
|
+
en: deepmerge.all([
|
|
81
|
+
en.numberFormats
|
|
82
|
+
])
|
|
83
|
+
},
|
|
84
|
+
datetimeFormats: {
|
|
85
|
+
en: deepmerge.all([
|
|
86
|
+
en.datetimeFormats
|
|
87
|
+
])
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Edge :edge="edge">
|
|
3
|
+
<template #default="{ src, dest }">
|
|
4
|
+
<EdgeBezierCurve :src="src" :dest="dest" v-slot="{ bezierPath }" >
|
|
5
|
+
<path :d="bezierPath" stroke="black" fill="none" stroke-width="10" stroke-linecap="round" />
|
|
6
|
+
<path :d="bezierPath" stroke="rgb(76, 175, 80)" fill="none" stroke-width="9" stroke-linecap="round"
|
|
7
|
+
:data-testid="`delete-edge-${edgeTestId}`" class="pointer-events-auto cursor-pointer"
|
|
8
|
+
@click.stop="flow.deleteEdge(edge)" />
|
|
9
|
+
</EdgeBezierCurve>
|
|
10
|
+
|
|
11
|
+
<EdgeEndHandle v-if="edge.src.node && edge.dest.node" :position="src" :edge="edge" end="src" />
|
|
12
|
+
<EdgeEndHandle v-if="edge.dest.node && edge.src.node" :position="dest" :edge="edge" end="dest" />
|
|
13
|
+
</template>
|
|
14
|
+
</Edge>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup>
|
|
18
|
+
|
|
19
|
+
import Edge from "../components/Edge.vue"
|
|
20
|
+
import EdgeBezierCurve from "../components/EdgeBezierCurve.vue"
|
|
21
|
+
import EdgeEndHandle from "../components/EdgeEndHandle.vue"
|
|
22
|
+
|
|
23
|
+
import { defineProps, toRefs, computed } from "vue"
|
|
24
|
+
import { useFlow } from "../components/index.js"
|
|
25
|
+
import { flowTestId } from "./flowTestId.js"
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
edge: {
|
|
29
|
+
type: Object,
|
|
30
|
+
required: true
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const { edge } = toRefs(props)
|
|
35
|
+
const flow = useFlow()
|
|
36
|
+
const edgeTestId = computed(() => flowTestId(edge.value?.id))
|
|
37
|
+
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Node :node="node">
|
|
3
|
+
<div class="demo-node-card">
|
|
4
|
+
<NodeHandle :node="node" class="demo-node-handle">
|
|
5
|
+
<i :class="['pi', icon, 'text-sm']" />
|
|
6
|
+
<span>{{ title }}</span>
|
|
7
|
+
</NodeHandle>
|
|
8
|
+
<div class="demo-node-body">
|
|
9
|
+
<slot />
|
|
10
|
+
</div>
|
|
11
|
+
<div class="demo-node-footer">
|
|
12
|
+
<Button @click="emit('delete')" size="small" severity="danger"
|
|
13
|
+
variant="outlined" rounded icon="pi pi-trash"
|
|
14
|
+
data-testid="delete-node" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</Node>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
|
|
22
|
+
import Node from "../components/Node.vue"
|
|
23
|
+
import NodeHandle from "../components/NodeHandle.vue"
|
|
24
|
+
|
|
25
|
+
import { defineProps, defineEmits, toRefs } from "vue"
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
node: {
|
|
29
|
+
type: Object,
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
title: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
icon: {
|
|
37
|
+
type: String,
|
|
38
|
+
required: true
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const { node, title, icon } = toRefs(props)
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits(['delete'])
|
|
45
|
+
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.demo-node-card {
|
|
50
|
+
background: var(--p-surface-0, #ffffff);
|
|
51
|
+
border: 1px solid var(--p-surface-200, #e5e7eb);
|
|
52
|
+
border-radius: 12px;
|
|
53
|
+
box-shadow:
|
|
54
|
+
0 4px 10px rgba(0, 0, 0, 0.12),
|
|
55
|
+
0 1px 5px rgba(0, 0, 0, 0.12);
|
|
56
|
+
min-width: 13rem;
|
|
57
|
+
max-width: 16rem;
|
|
58
|
+
color: var(--p-surface-900, #212121);
|
|
59
|
+
}
|
|
60
|
+
.demo-node-handle {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 0.4rem;
|
|
64
|
+
font-size: 1.125rem;
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
padding: 0.5rem 0.75rem 0.45rem;
|
|
67
|
+
margin-bottom: 0.15rem;
|
|
68
|
+
border-bottom: 1px solid var(--p-surface-200, #f3f4f6);
|
|
69
|
+
}
|
|
70
|
+
.demo-node-body {
|
|
71
|
+
padding-bottom: 0.15rem;
|
|
72
|
+
}
|
|
73
|
+
.demo-node-section {
|
|
74
|
+
font-size: 0.75rem;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
letter-spacing: 0.04em;
|
|
77
|
+
text-transform: uppercase;
|
|
78
|
+
color: var(--p-surface-500, #6b7280);
|
|
79
|
+
padding: 0.25rem 0.75rem 0.15rem;
|
|
80
|
+
}
|
|
81
|
+
.demo-node-footer {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
padding: 0.25rem 0 0.5rem;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative w-4 h-4 shrink-0">
|
|
3
|
+
<NodePort :node="node" :portId="portId" :direction="{ x, y: 0 }"
|
|
4
|
+
class="absolute rounded-full bg-green-500 w-4 h-4 border border-white"
|
|
5
|
+
:style="{ left: `${-0.5 + x * 0.3}rem`, top: `-0.5rem` }"
|
|
6
|
+
:data-testid="`port-${flowTestId(node.id)}-${portId}`">
|
|
7
|
+
</NodePort>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
|
|
13
|
+
import { defineProps, toRefs } from "vue"
|
|
14
|
+
import NodePort from "../components/NodePort.vue"
|
|
15
|
+
import { flowTestId } from "./flowTestId.js"
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
x: {
|
|
19
|
+
type: Number,
|
|
20
|
+
required: true
|
|
21
|
+
},
|
|
22
|
+
node: {
|
|
23
|
+
type: Object,
|
|
24
|
+
required: true
|
|
25
|
+
},
|
|
26
|
+
portId: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const { x, node, portId } = toRefs(props)
|
|
33
|
+
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Enrich" icon="pi-cog" @delete="deleteNode">
|
|
3
|
+
<div class="demo-node-section">Add field</div>
|
|
4
|
+
<div class="flex items-center p-1">
|
|
5
|
+
<DemoPort :x="-1" :node="node" portId="in" />
|
|
6
|
+
<div class="flex-1 mx-2">
|
|
7
|
+
<InputText v-model="node.field" class="w-full text-sm mb-2" placeholder="field name" />
|
|
8
|
+
<InputText v-model="node.value" class="w-full text-sm" placeholder="value" />
|
|
9
|
+
</div>
|
|
10
|
+
<DemoPort :x="1" :node="node" portId="out" />
|
|
11
|
+
</div>
|
|
12
|
+
</DemoNodeCard>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
|
|
17
|
+
import DemoNodeCard from "./DemoNodeCard.vue"
|
|
18
|
+
import DemoPort from "./DemoPort.vue"
|
|
19
|
+
import { useFlow } from "../components/index.js"
|
|
20
|
+
|
|
21
|
+
import { defineProps, toRefs } from "vue"
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
node: {
|
|
25
|
+
type: Object,
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { node } = toRefs(props)
|
|
31
|
+
const flow = useFlow()
|
|
32
|
+
|
|
33
|
+
function deleteNode() {
|
|
34
|
+
flow.deleteNode(node.value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full h-screen flex flex-col">
|
|
3
|
+
<NavBar />
|
|
4
|
+
<div class="bg-surface-0 dark:bg-surface-900 py-2 px-4 shadow flex items-center gap-2 z-10 flex-wrap">
|
|
5
|
+
<Button label="Add source" icon="pi pi-plus" size="small" data-testid="add-source"
|
|
6
|
+
@click="addNode('source')" />
|
|
7
|
+
<Button label="Add enrich" icon="pi pi-plus" size="small" severity="secondary"
|
|
8
|
+
data-testid="add-enrich" @click="addNode('enrich')" />
|
|
9
|
+
<Button label="Add output" icon="pi pi-plus" size="small" severity="secondary"
|
|
10
|
+
data-testid="add-output" @click="addNode('output')" />
|
|
11
|
+
<Button label="Run sample" icon="pi pi-play" size="small" severity="success"
|
|
12
|
+
data-testid="run-sample" @click="runSample" />
|
|
13
|
+
<Button label="Reset" icon="pi pi-refresh" size="small" severity="warning" variant="outlined" @click="resetGraph" />
|
|
14
|
+
<span v-if="runError" class="text-red-600 text-sm ml-2">{{ runError }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex flex-1 min-h-0">
|
|
17
|
+
<Flow class="flex-1 grow">
|
|
18
|
+
<template #default>
|
|
19
|
+
<component v-for="node of nodes" :key="node.id" :is="nodeComponentByType[node.type]" :node="node"
|
|
20
|
+
:style="{ left: node.position.x + 'px', top: node.position.y + 'px' }"
|
|
21
|
+
class="absolute" :data-testid="`node-${node.type}`">
|
|
22
|
+
</component>
|
|
23
|
+
</template>
|
|
24
|
+
<template #foreground-edges>
|
|
25
|
+
<DemoEdge v-for="edge of edges" :key="edge.id" :edge="edge" />
|
|
26
|
+
</template>
|
|
27
|
+
<template #free-edge="{ freeEdge }">
|
|
28
|
+
<DemoEdge v-if="freeEdge" :edge="freeEdge" />
|
|
29
|
+
</template>
|
|
30
|
+
</Flow>
|
|
31
|
+
<aside class="w-80 bg-surface-100 dark:bg-surface-800 border-l border-gray-200 overflow-auto p-3">
|
|
32
|
+
<div class="font-semibold mb-2">Last run</div>
|
|
33
|
+
<pre class="text-xs m-0 whitespace-pre-wrap" data-testid="last-run">{{ lastRunText }}</pre>
|
|
34
|
+
</aside>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
|
|
41
|
+
import Flow from '../components/Flow.vue'
|
|
42
|
+
import { useFlow } from '../components/index.js'
|
|
43
|
+
import NavBar from "../NavBar.vue"
|
|
44
|
+
import SourceNode from "./SourceNode.vue"
|
|
45
|
+
import EnrichNode from "./EnrichNode.vue"
|
|
46
|
+
import OutputNode from "./OutputNode.vue"
|
|
47
|
+
import DemoEdge from "./DemoEdge.vue"
|
|
48
|
+
import { createSeedGraph, STORAGE_KEY } from "./seedGraph.js"
|
|
49
|
+
import { runDemoGraph } from "./runDemoGraph.js"
|
|
50
|
+
|
|
51
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
52
|
+
|
|
53
|
+
const nodeComponentByType = {
|
|
54
|
+
source: SourceNode,
|
|
55
|
+
enrich: EnrichNode,
|
|
56
|
+
output: OutputNode
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const seed = createSeedGraph()
|
|
60
|
+
const nodes = ref(seed.nodes)
|
|
61
|
+
const edges = ref(seed.edges)
|
|
62
|
+
const lastRun = ref(null)
|
|
63
|
+
const runError = ref('')
|
|
64
|
+
|
|
65
|
+
const lastRunText = computed(() => {
|
|
66
|
+
if (!lastRun.value) return 'Press Run sample to execute the graph on the source record.'
|
|
67
|
+
return JSON.stringify(lastRun.value, null, 2)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
function isConnectable(fromNode, fromPort, toNode, toPort) {
|
|
71
|
+
const fromId = typeof fromNode == 'string' ? fromNode : fromNode?.id
|
|
72
|
+
const toId = typeof toNode == 'string' ? toNode : toNode?.id
|
|
73
|
+
if (fromId && toId && fromId === toId) return false
|
|
74
|
+
if (!fromPort || !toPort) return true
|
|
75
|
+
return fromPort !== toPort
|
|
76
|
+
&& (fromPort === 'in' || fromPort === 'out')
|
|
77
|
+
&& (toPort === 'in' || toPort === 'out')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function connect(newEdge) {
|
|
81
|
+
if (newEdge.src.port !== 'out' && newEdge.dest.port === 'out') {
|
|
82
|
+
const tmp = newEdge.src
|
|
83
|
+
newEdge.src = newEdge.dest
|
|
84
|
+
newEdge.dest = tmp
|
|
85
|
+
}
|
|
86
|
+
edges.value.push(newEdge)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const flow = useFlow({
|
|
90
|
+
nodes,
|
|
91
|
+
edges,
|
|
92
|
+
width: 1600,
|
|
93
|
+
height: 900,
|
|
94
|
+
edgeConnectDistance: 48,
|
|
95
|
+
isConnectable,
|
|
96
|
+
connect
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function persistableNodes() {
|
|
100
|
+
return nodes.value.map(node => {
|
|
101
|
+
const { lastRecord, ...rest } = node
|
|
102
|
+
return rest
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onMounted(() => {
|
|
107
|
+
try {
|
|
108
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
109
|
+
if (raw) {
|
|
110
|
+
const parsed = JSON.parse(raw)
|
|
111
|
+
if (Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) {
|
|
112
|
+
nodes.value = parsed.nodes
|
|
113
|
+
edges.value = parsed.edges
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// keep seed graph
|
|
118
|
+
}
|
|
119
|
+
watch([nodes, edges], () => {
|
|
120
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
121
|
+
nodes: persistableNodes(),
|
|
122
|
+
edges: edges.value
|
|
123
|
+
}))
|
|
124
|
+
}, { deep: true })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
function addNode(type) {
|
|
128
|
+
const id = flow.generateNodeId()
|
|
129
|
+
const node = {
|
|
130
|
+
id,
|
|
131
|
+
type,
|
|
132
|
+
position: {
|
|
133
|
+
x: 80 - flow.position.x,
|
|
134
|
+
y: 80 - flow.position.y
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (type === 'source') node.record = { sku: 'NEW', qty: 1 }
|
|
138
|
+
if (type === 'enrich') {
|
|
139
|
+
node.field = 'extra'
|
|
140
|
+
node.value = ''
|
|
141
|
+
}
|
|
142
|
+
nodes.value.push(node)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resetGraph() {
|
|
146
|
+
const next = createSeedGraph()
|
|
147
|
+
nodes.value = next.nodes
|
|
148
|
+
edges.value = next.edges
|
|
149
|
+
lastRun.value = null
|
|
150
|
+
runError.value = ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runSample() {
|
|
154
|
+
const result = runDemoGraph(nodes.value, edges.value)
|
|
155
|
+
lastRun.value = result
|
|
156
|
+
runError.value = result.error || ''
|
|
157
|
+
if (result.traces) {
|
|
158
|
+
for (const trace of result.traces) {
|
|
159
|
+
const node = nodes.value.find(n => n.id === trace.nodeId)
|
|
160
|
+
if (node) node.lastRecord = trace.record
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DemoNodeCard :node="node" title="Output" icon="pi-box" @delete="deleteNode">
|
|
3
|
+
<div class="demo-node-section">Result</div>
|
|
4
|
+
<div class="flex items-center p-1">
|
|
5
|
+
<DemoPort :x="-1" :node="node" portId="in" />
|
|
6
|
+
<pre class="demo-node-result flex-1 mx-2 my-0 overflow-auto">{{
|
|
7
|
+
JSON.stringify(node.lastRecord ?? {}, null, 2)
|
|
8
|
+
}}</pre>
|
|
9
|
+
</div>
|
|
10
|
+
</DemoNodeCard>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
|
|
15
|
+
import DemoNodeCard from "./DemoNodeCard.vue"
|
|
16
|
+
import DemoPort from "./DemoPort.vue"
|
|
17
|
+
import { useFlow } from "../components/index.js"
|
|
18
|
+
|
|
19
|
+
import { defineProps, toRefs } from "vue"
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
node: {
|
|
23
|
+
type: Object,
|
|
24
|
+
required: true
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const { node } = toRefs(props)
|
|
29
|
+
const flow = useFlow()
|
|
30
|
+
|
|
31
|
+
function deleteNode() {
|
|
32
|
+
flow.deleteNode(node.value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.demo-node-result {
|
|
39
|
+
font-size: 0.75rem;
|
|
40
|
+
max-height: 8rem;
|
|
41
|
+
background: var(--p-surface-50, #f9fafb);
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
padding: 0.4rem 0.5rem;
|
|
44
|
+
}
|
|
45
|
+
</style>
|