darabos commited on
Commit
5882a26
·
1 Parent(s): b5a8a95

Switch to Svelte Query. It works!

Browse files
server/basic_ops.py CHANGED
@@ -16,7 +16,7 @@ def create_scale_free_graph(*, nodes: int = 10):
16
 
17
  @ops.op("Compute PageRank")
18
  @ops.nx_node_attribute_func('pagerank')
19
- def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=3):
20
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
21
 
22
 
@@ -35,9 +35,9 @@ def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = Non
35
  nodes = nodes.to_records()
36
  edges = graph.dfs['edges'].drop_duplicates(['source', 'target'])
37
  edges = edges.to_records()
38
- pos = nx.spring_layout(graph.to_nx())
39
  v = {
40
- 'animationDuration': 1500,
41
  'animationEasingUpdate': 'quinticInOut',
42
  'series': [
43
  {
@@ -56,7 +56,7 @@ def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = Non
56
  'data': [
57
  {
58
  'id': str(n.id),
59
- 'x': pos[n.id][0], 'y': pos[n.id][1],
60
  # Adjust node size to cover the same area no matter how many nodes there are.
61
  'symbolSize': 50 / len(nodes) ** 0.5,
62
  'itemStyle': {'color': n.color} if color_nodes_by else {},
 
16
 
17
  @ops.op("Compute PageRank")
18
  @ops.nx_node_attribute_func('pagerank')
19
+ def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
20
  return nx.pagerank(graph, alpha=damping, max_iter=iterations)
21
 
22
 
 
35
  nodes = nodes.to_records()
36
  edges = graph.dfs['edges'].drop_duplicates(['source', 'target'])
37
  edges = edges.to_records()
38
+ pos = nx.spring_layout(graph.to_nx(), iterations=max(1, int(10000/len(nodes))))
39
  v = {
40
+ 'animationDuration': 500,
41
  'animationEasingUpdate': 'quinticInOut',
42
  'series': [
43
  {
 
56
  'data': [
57
  {
58
  'id': str(n.id),
59
+ 'x': float(pos[n.id][0]), 'y': float(pos[n.id][1]),
60
  # Adjust node size to cover the same area no matter how many nodes there are.
61
  'symbolSize': 50 / len(nodes) ** 0.5,
62
  'itemStyle': {'color': n.color} if color_nodes_by else {},
server/workspace.py CHANGED
@@ -83,7 +83,7 @@ def save(ws: Workspace, path: str):
83
  j = ws.model_dump_json(indent=2)
84
  dirname, basename = os.path.split(path)
85
  # Create temp file in the same directory to make sure it's on the same filesystem.
86
- with tempfile.NamedTemporaryFile('w', prefix=basename, dir=dirname, delete_on_close=False) as f:
87
  f.write(j)
88
  f.close()
89
  os.replace(f.name, path)
 
83
  j = ws.model_dump_json(indent=2)
84
  dirname, basename = os.path.split(path)
85
  # Create temp file in the same directory to make sure it's on the same filesystem.
86
+ with tempfile.NamedTemporaryFile('w', prefix=f'.{basename}.', dir=dirname, delete_on_close=False) as f:
87
  f.write(j)
88
  f.close()
89
  os.replace(f.name, path)
web/package-lock.json CHANGED
@@ -10,6 +10,7 @@
10
  "dependencies": {
11
  "@iconify-json/tabler": "^1.1.110",
12
  "@popperjs/core": "^2.11.8",
 
13
  "@xyflow/svelte": "^0.1.3",
14
  "bootstrap": "^5.3.3",
15
  "echarts": "^5.5.0",
@@ -369,6 +370,19 @@
369
  "vite": "^5.0.0"
370
  }
371
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  "node_modules/@tsconfig/svelte": {
373
  "version": "5.0.4",
374
  "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
@@ -2585,6 +2599,12 @@
2585
  "debug": "^4.3.4"
2586
  }
2587
  },
 
 
 
 
 
 
2588
  "@tsconfig/svelte": {
2589
  "version": "5.0.4",
2590
  "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
 
10
  "dependencies": {
11
  "@iconify-json/tabler": "^1.1.110",
12
  "@popperjs/core": "^2.11.8",
13
+ "@sveltestack/svelte-query": "^1.6.0",
14
  "@xyflow/svelte": "^0.1.3",
15
  "bootstrap": "^5.3.3",
16
  "echarts": "^5.5.0",
 
370
  "vite": "^5.0.0"
371
  }
372
  },
373
+ "node_modules/@sveltestack/svelte-query": {
374
+ "version": "1.6.0",
375
+ "resolved": "https://registry.npmjs.org/@sveltestack/svelte-query/-/svelte-query-1.6.0.tgz",
376
+ "integrity": "sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==",
377
+ "peerDependencies": {
378
+ "broadcast-channel": "^4.5.0"
379
+ },
380
+ "peerDependenciesMeta": {
381
+ "broadcast-channel": {
382
+ "optional": true
383
+ }
384
+ }
385
+ },
386
  "node_modules/@tsconfig/svelte": {
387
  "version": "5.0.4",
388
  "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
 
2599
  "debug": "^4.3.4"
2600
  }
2601
  },
2602
+ "@sveltestack/svelte-query": {
2603
+ "version": "1.6.0",
2604
+ "resolved": "https://registry.npmjs.org/@sveltestack/svelte-query/-/svelte-query-1.6.0.tgz",
2605
+ "integrity": "sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==",
2606
+ "requires": {}
2607
+ },
2608
  "@tsconfig/svelte": {
2609
  "version": "5.0.4",
2610
  "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
web/package.json CHANGED
@@ -23,6 +23,7 @@
23
  "dependencies": {
24
  "@iconify-json/tabler": "^1.1.110",
25
  "@popperjs/core": "^2.11.8",
 
26
  "@xyflow/svelte": "^0.1.3",
27
  "bootstrap": "^5.3.3",
28
  "echarts": "^5.5.0",
 
23
  "dependencies": {
24
  "@iconify-json/tabler": "^1.1.110",
25
  "@popperjs/core": "^2.11.8",
26
+ "@sveltestack/svelte-query": "^1.6.0",
27
  "@xyflow/svelte": "^0.1.3",
28
  "bootstrap": "^5.3.3",
29
  "echarts": "^5.5.0",
web/src/LynxKiteFlow.svelte CHANGED
@@ -14,6 +14,7 @@
14
  type Connection,
15
  type NodeTypes,
16
  } from '@xyflow/svelte';
 
17
  import NodeWithParams from './NodeWithParams.svelte';
18
  import NodeWithParamsVertical from './NodeWithParamsVertical.svelte';
19
  import NodeWithVisualization from './NodeWithVisualization.svelte';
@@ -23,7 +24,26 @@
23
  import NodeSearch from './NodeSearch.svelte';
24
  import '@xyflow/svelte/dist/style.css';
25
 
 
 
26
  const { screenToFlowPosition } = useSvelteFlow();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  const nodeTypes: NodeTypes = {
29
  basic: NodeWithParams,
@@ -34,20 +54,15 @@
34
  area: NodeWithArea,
35
  };
36
 
37
- export let path = '';
38
  const nodes = writable<Node[]>([]);
39
  const edges = writable<Edge[]>([]);
40
- let workspaceLoaded = false;
41
- async function fetchWorkspace(path) {
42
- if (!path) return;
43
- const res = await fetch(`/api/load?path=${path}`);
44
- const j = await res.json();
45
- nodes.set(j.nodes);
46
- edges.set(j.edges);
47
- backendWorkspace = orderedJSON(j);
48
- workspaceLoaded = true;
49
  }
50
- $: fetchWorkspace(path);
51
 
52
  function closeNodeSearch() {
53
  nodeSearchSettings = undefined;
@@ -102,7 +117,6 @@
102
  };
103
 
104
  const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
105
- let backendWorkspace: string;
106
  // Like JSON.stringify, but with keys sorted.
107
  function orderedJSON(obj: any) {
108
  const allKeys = new Set();
@@ -110,29 +124,18 @@
110
  return JSON.stringify(obj, Array.from(allKeys).sort());
111
  }
112
  graph.subscribe(async (g) => {
113
- if (!workspaceLoaded) {
114
- return;
115
- }
116
  const dragging = g.nodes.find((n) => n.dragging);
117
  if (dragging) return;
118
  g = JSON.parse(JSON.stringify(g));
119
  for (const node of g.nodes) {
120
  delete node.computed;
 
121
  }
122
  const ws = orderedJSON(g);
123
- if (ws === backendWorkspace) return;
124
- // console.log('save', '\n' + ws, '\n' + backendWorkspace);
125
- backendWorkspace = ws;
126
- const res = await fetch('/api/save', {
127
- method: 'POST',
128
- headers: {
129
- 'Content-Type': 'application/json',
130
- },
131
- body: JSON.stringify({ path, ws: g }),
132
- });
133
- const j = await res.json();
134
- backendWorkspace = orderedJSON(j);
135
- nodes.set(j.nodes);
136
  });
137
  function onconnect(connection: Connection) {
138
  edges.update((edges) => {
@@ -169,7 +172,7 @@
169
  on:paneclick={toggleNodeSearch}
170
  on:nodeclick={nodeClick}
171
  proOptions={{ hideAttribution: true }}
172
- maxZoom={1.5}
173
  minZoom={0.3}
174
  onconnect={onconnect}
175
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
 
14
  type Connection,
15
  type NodeTypes,
16
  } from '@xyflow/svelte';
17
+ import { useQuery, useMutation, useQueryClient } from '@sveltestack/svelte-query';
18
  import NodeWithParams from './NodeWithParams.svelte';
19
  import NodeWithParamsVertical from './NodeWithParamsVertical.svelte';
20
  import NodeWithVisualization from './NodeWithVisualization.svelte';
 
24
  import NodeSearch from './NodeSearch.svelte';
25
  import '@xyflow/svelte/dist/style.css';
26
 
27
+ export let path = '';
28
+
29
  const { screenToFlowPosition } = useSvelteFlow();
30
+ const queryClient = useQueryClient();
31
+ const backendWorkspace = useQuery(['workspace', path], async () => {
32
+ const res = await fetch(`/api/load?path=${path}`);
33
+ return res.json();
34
+ }, {staleTime: 10000});
35
+ const mutation = useMutation(async(update) => {
36
+ const res = await fetch('/api/save', {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ },
41
+ body: JSON.stringify(update),
42
+ });
43
+ return await res.json();
44
+ }, {
45
+ onSuccess: data => queryClient.setQueryData(['workspace', path], data),
46
+ })
47
 
48
  const nodeTypes: NodeTypes = {
49
  basic: NodeWithParams,
 
54
  area: NodeWithArea,
55
  };
56
 
 
57
  const nodes = writable<Node[]>([]);
58
  const edges = writable<Edge[]>([]);
59
+ let doNotSave = true;
60
+ $: if ($backendWorkspace.isSuccess) {
61
+ doNotSave = true; // Change is coming from the backend.
62
+ nodes.set(JSON.parse(JSON.stringify($backendWorkspace.data?.nodes || [])));
63
+ edges.set(JSON.parse(JSON.stringify($backendWorkspace.data?.edges || [])));
64
+ doNotSave = false;
 
 
 
65
  }
 
66
 
67
  function closeNodeSearch() {
68
  nodeSearchSettings = undefined;
 
117
  };
118
 
119
  const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
 
120
  // Like JSON.stringify, but with keys sorted.
121
  function orderedJSON(obj: any) {
122
  const allKeys = new Set();
 
124
  return JSON.stringify(obj, Array.from(allKeys).sort());
125
  }
126
  graph.subscribe(async (g) => {
127
+ if (doNotSave) return;
 
 
128
  const dragging = g.nodes.find((n) => n.dragging);
129
  if (dragging) return;
130
  g = JSON.parse(JSON.stringify(g));
131
  for (const node of g.nodes) {
132
  delete node.computed;
133
+ delete node.selected;
134
  }
135
  const ws = orderedJSON(g);
136
+ const bd = orderedJSON($backendWorkspace.data);
137
+ if (ws === bd) return;
138
+ $mutation.mutate({ path, ws: g });
 
 
 
 
 
 
 
 
 
 
139
  });
140
  function onconnect(connection: Connection) {
141
  edges.update((edges) => {
 
172
  on:paneclick={toggleNodeSearch}
173
  on:nodeclick={nodeClick}
174
  proOptions={{ hideAttribution: true }}
175
+ maxZoom={3}
176
  minZoom={0.3}
177
  onconnect={onconnect}
178
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
web/src/NodeWithVisualization.svelte CHANGED
@@ -8,7 +8,9 @@
8
  </script>
9
 
10
  <NodeWithParams {...$$props}>
11
- <Chart {init} options={data.view} style="width: 250px; height: 250px" />
 
 
12
  </NodeWithParams>
13
  <style>
14
  </style>
 
8
  </script>
9
 
10
  <NodeWithParams {...$$props}>
11
+ {#if data.view}
12
+ <Chart {init} options={data.view} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
13
+ {/if}
14
  </NodeWithParams>
15
  <style>
16
  </style>
web/src/Workspace.svelte CHANGED
@@ -1,5 +1,6 @@
1
  <script lang="ts">
2
  // This is the whole LynxKite workspace editor page.
 
3
  import { SvelteFlowProvider } from '@xyflow/svelte';
4
  import ArrowBack from 'virtual:icons/tabler/arrow-back'
5
  import Backspace from 'virtual:icons/tabler/backspace'
@@ -7,24 +8,27 @@
7
  import LynxKiteFlow from './LynxKiteFlow.svelte';
8
  export let path = '';
9
  $: parent = path.split('/').slice(0, -1).join('/');
 
10
  </script>
11
 
12
- <div class="page">
13
- <div class="top-bar">
14
- <div class="ws-name">
15
- <a href><img src="/favicon.ico"></a>
16
- {path}
17
- </div>
18
- <div class="tools">
19
- <a href><Atom /></a>
20
- <a href><Backspace /></a>
21
- <a href="#dir?path={parent}"><ArrowBack /></a>
 
 
22
  </div>
 
 
 
23
  </div>
24
- <SvelteFlowProvider>
25
- <LynxKiteFlow path={path} />
26
- </SvelteFlowProvider>
27
- </div>
28
 
29
  <style>
30
  .top-bar {
 
1
  <script lang="ts">
2
  // This is the whole LynxKite workspace editor page.
3
+ import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query'
4
  import { SvelteFlowProvider } from '@xyflow/svelte';
5
  import ArrowBack from 'virtual:icons/tabler/arrow-back'
6
  import Backspace from 'virtual:icons/tabler/backspace'
 
8
  import LynxKiteFlow from './LynxKiteFlow.svelte';
9
  export let path = '';
10
  $: parent = path.split('/').slice(0, -1).join('/');
11
+ const queryClient = new QueryClient()
12
  </script>
13
 
14
+ <QueryClientProvider client={queryClient}>
15
+ <div class="page">
16
+ <div class="top-bar">
17
+ <div class="ws-name">
18
+ <a href><img src="/favicon.ico"></a>
19
+ {path}
20
+ </div>
21
+ <div class="tools">
22
+ <a href><Atom /></a>
23
+ <a href><Backspace /></a>
24
+ <a href="#dir?path={parent}"><ArrowBack /></a>
25
+ </div>
26
  </div>
27
+ <SvelteFlowProvider>
28
+ <LynxKiteFlow path={path} />
29
+ </SvelteFlowProvider>
30
  </div>
31
+ </QueryClientProvider>
 
 
 
32
 
33
  <style>
34
  .top-bar {