Spaces:
Running
Running
Create nodes at the search location.
Browse files- web/src/App.svelte +5 -94
- web/src/LynxKiteFlow.svelte +131 -0
- web/src/NodeSearch.svelte +6 -5
web/src/App.svelte
CHANGED
@@ -1,99 +1,10 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import { writable } from 'svelte/store';
|
3 |
import {
|
4 |
-
|
5 |
-
Controls,
|
6 |
-
Background,
|
7 |
-
MiniMap,
|
8 |
-
MarkerType,
|
9 |
-
Position,
|
10 |
-
type Node,
|
11 |
-
type Edge,
|
12 |
} from '@xyflow/svelte';
|
13 |
-
import
|
14 |
-
import NodeSearch from './NodeSearch.svelte';
|
15 |
-
import '@xyflow/svelte/dist/style.css';
|
16 |
-
|
17 |
-
const nodeTypes: NodeTypes = {
|
18 |
-
basic: LynxKiteNode,
|
19 |
-
};
|
20 |
-
|
21 |
-
const nodes = writable<Node[]>([
|
22 |
-
{
|
23 |
-
id: '1',
|
24 |
-
type: 'basic',
|
25 |
-
data: { title: 'Compute PageRank', params: { damping: 0.85, iterations: 3 } },
|
26 |
-
position: { x: 0, y: 0 },
|
27 |
-
sourcePosition: Position.Right,
|
28 |
-
targetPosition: Position.Left,
|
29 |
-
},
|
30 |
-
{
|
31 |
-
id: '3',
|
32 |
-
type: 'basic',
|
33 |
-
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
34 |
-
position: { x: -300, y: 0 },
|
35 |
-
sourcePosition: Position.Right,
|
36 |
-
},
|
37 |
-
]);
|
38 |
-
|
39 |
-
const edges = writable<Edge[]>([
|
40 |
-
{
|
41 |
-
id: '3-1',
|
42 |
-
source: '3',
|
43 |
-
target: '1',
|
44 |
-
markerEnd: {
|
45 |
-
type: MarkerType.ArrowClosed,
|
46 |
-
},
|
47 |
-
},
|
48 |
-
]);
|
49 |
-
|
50 |
-
function closeNodeSearch() {
|
51 |
-
nodeSearchPos = undefined;
|
52 |
-
}
|
53 |
-
function toggleNodeSearch({ detail: { event } }) {
|
54 |
-
if (nodeSearchPos) {
|
55 |
-
closeNodeSearch();
|
56 |
-
return;
|
57 |
-
}
|
58 |
-
event.preventDefault();
|
59 |
-
const width = 500;
|
60 |
-
const height = 200;
|
61 |
-
nodeSearchPos = {
|
62 |
-
top: event.clientY < height - 200 ? event.clientY : undefined,
|
63 |
-
left: event.clientX < width - 200 ? event.clientX : undefined,
|
64 |
-
right: event.clientX >= width - 200 ? width - event.clientX : undefined,
|
65 |
-
bottom: event.clientY >= height - 200 ? height - event.clientY : undefined
|
66 |
-
};
|
67 |
-
nodeSearchPos = {
|
68 |
-
top: event.clientY,
|
69 |
-
left: event.clientX - 150,
|
70 |
-
};
|
71 |
-
}
|
72 |
-
function addNode(node: Node) {
|
73 |
-
nodes.update((n) => [...n, node]);
|
74 |
-
}
|
75 |
-
|
76 |
-
const boxes = [
|
77 |
-
{ name: 'Compute PageRank' },
|
78 |
-
{ name: 'Import Parquet' },
|
79 |
-
{ name: 'Export Parquet' },
|
80 |
-
{ name: 'Export CSV' },
|
81 |
-
];
|
82 |
-
|
83 |
-
let nodeSearchPos;
|
84 |
</script>
|
85 |
|
86 |
-
<
|
87 |
-
<
|
88 |
-
|
89 |
-
proOptions={{ hideAttribution: true }}
|
90 |
-
maxZoom={1.5}
|
91 |
-
minZoom={0.3}
|
92 |
-
>
|
93 |
-
<Background />
|
94 |
-
<Controls />
|
95 |
-
<Background />
|
96 |
-
<MiniMap />
|
97 |
-
{#if nodeSearchPos}<NodeSearch boxes={boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
98 |
-
</SvelteFlow>
|
99 |
-
</div>
|
|
|
1 |
<script lang="ts">
|
|
|
2 |
import {
|
3 |
+
SvelteFlowProvider,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
} from '@xyflow/svelte';
|
5 |
+
import LynxKiteFlow from './LynxKiteFlow.svelte';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
</script>
|
7 |
|
8 |
+
<SvelteFlowProvider>
|
9 |
+
<LynxKiteFlow />
|
10 |
+
</SvelteFlowProvider>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/LynxKiteFlow.svelte
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { writable } from 'svelte/store';
|
3 |
+
import {
|
4 |
+
SvelteFlow,
|
5 |
+
Controls,
|
6 |
+
Background,
|
7 |
+
MiniMap,
|
8 |
+
MarkerType,
|
9 |
+
Position,
|
10 |
+
useSvelteFlow,
|
11 |
+
type XYPosition,
|
12 |
+
type Node,
|
13 |
+
type Edge,
|
14 |
+
} from '@xyflow/svelte';
|
15 |
+
import LynxKiteNode from './LynxKiteNode.svelte';
|
16 |
+
import NodeSearch from './NodeSearch.svelte';
|
17 |
+
import '@xyflow/svelte/dist/style.css';
|
18 |
+
|
19 |
+
const { screenToFlowPosition } = useSvelteFlow();
|
20 |
+
const nodeTypes: NodeTypes = {
|
21 |
+
basic: LynxKiteNode,
|
22 |
+
};
|
23 |
+
|
24 |
+
const nodes = writable<Node[]>([
|
25 |
+
{
|
26 |
+
id: '1',
|
27 |
+
type: 'basic',
|
28 |
+
data: { title: 'Compute PageRank', params: { damping: 0.85, iterations: 3 } },
|
29 |
+
position: { x: 0, y: 0 },
|
30 |
+
sourcePosition: Position.Right,
|
31 |
+
targetPosition: Position.Left,
|
32 |
+
},
|
33 |
+
{
|
34 |
+
id: '3',
|
35 |
+
type: 'basic',
|
36 |
+
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
37 |
+
position: { x: -300, y: 0 },
|
38 |
+
sourcePosition: Position.Right,
|
39 |
+
},
|
40 |
+
]);
|
41 |
+
|
42 |
+
const edges = writable<Edge[]>([
|
43 |
+
{
|
44 |
+
id: '3-1',
|
45 |
+
source: '3',
|
46 |
+
target: '1',
|
47 |
+
markerEnd: {
|
48 |
+
type: MarkerType.ArrowClosed,
|
49 |
+
},
|
50 |
+
},
|
51 |
+
]);
|
52 |
+
|
53 |
+
function closeNodeSearch() {
|
54 |
+
nodeSearchPos = undefined;
|
55 |
+
}
|
56 |
+
function toggleNodeSearch({ detail: { event } }) {
|
57 |
+
if (nodeSearchPos) {
|
58 |
+
closeNodeSearch();
|
59 |
+
return;
|
60 |
+
}
|
61 |
+
event.preventDefault();
|
62 |
+
const width = 500;
|
63 |
+
const height = 200;
|
64 |
+
nodeSearchPos = {
|
65 |
+
top: event.clientY < height - 200 ? event.clientY : undefined,
|
66 |
+
left: event.clientX < width - 200 ? event.clientX : undefined,
|
67 |
+
right: event.clientX >= width - 200 ? width - event.clientX : undefined,
|
68 |
+
bottom: event.clientY >= height - 200 ? height - event.clientY : undefined
|
69 |
+
};
|
70 |
+
nodeSearchPos = {
|
71 |
+
top: event.clientY,
|
72 |
+
left: event.clientX - 150,
|
73 |
+
};
|
74 |
+
}
|
75 |
+
function addNode(e) {
|
76 |
+
const node = {...e.detail};
|
77 |
+
nodes.update((n) => {
|
78 |
+
node.position = screenToFlowPosition({x: nodeSearchPos.left, y: nodeSearchPos.top});
|
79 |
+
const title = node.data.title;
|
80 |
+
let i = 1;
|
81 |
+
node.id = `${title} ${i}`;
|
82 |
+
while (n.find((x) => x.id === node.id)) {
|
83 |
+
i += 1;
|
84 |
+
node.id = `${title} ${i}`;
|
85 |
+
}
|
86 |
+
return [...n, node]
|
87 |
+
});
|
88 |
+
closeNodeSearch();
|
89 |
+
}
|
90 |
+
|
91 |
+
const boxes = [
|
92 |
+
{
|
93 |
+
type: 'basic',
|
94 |
+
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
95 |
+
sourcePosition: Position.Right,
|
96 |
+
},
|
97 |
+
{
|
98 |
+
type: 'basic',
|
99 |
+
data: { title: 'Export Parquet', params: { filename: '/tmp/x.parquet' } },
|
100 |
+
sourcePosition: Position.Right,
|
101 |
+
},
|
102 |
+
{
|
103 |
+
type: 'basic',
|
104 |
+
data: { title: 'Export CSV', params: { filename: '/tmp/x.csv' } },
|
105 |
+
sourcePosition: Position.Right,
|
106 |
+
},
|
107 |
+
{
|
108 |
+
type: 'basic',
|
109 |
+
data: { title: 'Compute PageRank', params: { damping: 0.85, iterations: 3 } },
|
110 |
+
sourcePosition: Position.Right,
|
111 |
+
targetPosition: Position.Left,
|
112 |
+
},
|
113 |
+
];
|
114 |
+
|
115 |
+
let nodeSearchPos: XYPosition | undefined = undefined
|
116 |
+
</script>
|
117 |
+
|
118 |
+
<div style:height="100vh">
|
119 |
+
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
120 |
+
on:paneclick={toggleNodeSearch}
|
121 |
+
proOptions={{ hideAttribution: true }}
|
122 |
+
maxZoom={1.5}
|
123 |
+
minZoom={0.3}
|
124 |
+
>
|
125 |
+
<Background />
|
126 |
+
<Controls />
|
127 |
+
<Background />
|
128 |
+
<MiniMap />
|
129 |
+
{#if nodeSearchPos}<NodeSearch boxes={boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
130 |
+
</SvelteFlow>
|
131 |
+
</div>
|
web/src/NodeSearch.svelte
CHANGED
@@ -9,12 +9,11 @@
|
|
9 |
let selectedIndex = 0;
|
10 |
onMount(() => searchBox.focus());
|
11 |
const fuse = new Fuse(boxes, {
|
12 |
-
keys: ['
|
13 |
})
|
14 |
function onInput() {
|
15 |
-
console.log('input', searchBox.value, selectedIndex);
|
16 |
hits = fuse.search(searchBox.value);
|
17 |
-
selectedIndex = Math.min(selectedIndex, hits.length - 1);
|
18 |
}
|
19 |
function onKeyDown(e) {
|
20 |
if (e.key === 'ArrowDown') {
|
@@ -24,7 +23,9 @@
|
|
24 |
e.preventDefault();
|
25 |
selectedIndex = Math.max(selectedIndex - 1, 0);
|
26 |
} else if (e.key === 'Enter') {
|
27 |
-
|
|
|
|
|
28 |
} else if (e.key === 'Escape') {
|
29 |
dispatch('cancel');
|
30 |
}
|
@@ -42,7 +43,7 @@ style="top: {pos.top}px; left: {pos.left}px; right: {pos.right}px; bottom: {pos.
|
|
42 |
on:focusout={() => dispatch('cancel')}
|
43 |
placeholder="Search for box">
|
44 |
{#each hits as box, index}
|
45 |
-
<div class="search-result" class:selected={index == selectedIndex}>{index} {box.item.
|
46 |
{/each}
|
47 |
</div>
|
48 |
|
|
|
9 |
let selectedIndex = 0;
|
10 |
onMount(() => searchBox.focus());
|
11 |
const fuse = new Fuse(boxes, {
|
12 |
+
keys: ['data.title']
|
13 |
})
|
14 |
function onInput() {
|
|
|
15 |
hits = fuse.search(searchBox.value);
|
16 |
+
selectedIndex = Math.max(0, Math.min(selectedIndex, hits.length - 1));
|
17 |
}
|
18 |
function onKeyDown(e) {
|
19 |
if (e.key === 'ArrowDown') {
|
|
|
23 |
e.preventDefault();
|
24 |
selectedIndex = Math.max(selectedIndex - 1, 0);
|
25 |
} else if (e.key === 'Enter') {
|
26 |
+
const node = {...hits[selectedIndex].item};
|
27 |
+
node.position = {x: pos.left, y: pos.top};
|
28 |
+
dispatch('add', node);
|
29 |
} else if (e.key === 'Escape') {
|
30 |
dispatch('cancel');
|
31 |
}
|
|
|
43 |
on:focusout={() => dispatch('cancel')}
|
44 |
placeholder="Search for box">
|
45 |
{#each hits as box, index}
|
46 |
+
<div class="search-result" class:selected={index == selectedIndex}>{index} {box.item.data.title}</div>
|
47 |
{/each}
|
48 |
</div>
|
49 |
|