Spaces:
Running
Running
Tri-state status. Nicer visuals.
Browse files
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
@@ -86,7 +86,7 @@ def clean_input(ws_pyd):
|
|
86 |
for node in ws_pyd.nodes:
|
87 |
node.data.display = None
|
88 |
node.data.error = None
|
89 |
-
node.data.
|
90 |
node.position.x = 0
|
91 |
node.position.y = 0
|
92 |
if node.model_extra:
|
@@ -228,7 +228,7 @@ async def execute(
|
|
228 |
for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
|
229 |
if "data" not in nc:
|
230 |
nc["data"] = pycrdt.Map()
|
231 |
-
nc["data"]["
|
232 |
# Nodes get a reference to their CRDT maps, so they can update them as the results come in.
|
233 |
np._crdt = nc
|
234 |
await workspace.execute(ws_pyd)
|
|
|
86 |
for node in ws_pyd.nodes:
|
87 |
node.data.display = None
|
88 |
node.data.error = None
|
89 |
+
node.data.status = workspace.NodeStatus.done
|
90 |
node.position.x = 0
|
91 |
node.position.y = 0
|
92 |
if node.model_extra:
|
|
|
228 |
for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
|
229 |
if "data" not in nc:
|
230 |
nc["data"] = pycrdt.Map()
|
231 |
+
nc["data"]["status"] = "planned"
|
232 |
# Nodes get a reference to their CRDT maps, so they can update them as the results come in.
|
233 |
np._crdt = nc
|
234 |
await workspace.execute(ws_pyd)
|
lynxkite-app/web/src/index.css
CHANGED
@@ -94,8 +94,19 @@ body {
|
|
94 |
padding: 8px;
|
95 |
}
|
96 |
|
97 |
-
.lynxkite-node .title.
|
98 |
-
background:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
}
|
100 |
|
101 |
.handle-name {
|
@@ -325,6 +336,20 @@ body {
|
|
325 |
}
|
326 |
}
|
327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
.react-flow__edge.selected path.react-flow__edge-path {
|
329 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
330 |
outline-offset: 10px;
|
|
|
94 |
padding: 8px;
|
95 |
}
|
96 |
|
97 |
+
.lynxkite-node .title.active {
|
98 |
+
background: linear-gradient(
|
99 |
+
to right,
|
100 |
+
oklch(75% 0.2 55),
|
101 |
+
oklch(90% 0.2 55),
|
102 |
+
oklch(75% 0.1 55)
|
103 |
+
);
|
104 |
+
background-size: 180% 180%;
|
105 |
+
animation: active-node-gradient-animation 2s ease-in-out infinite;
|
106 |
+
}
|
107 |
+
|
108 |
+
.lynxkite-node .title.planned {
|
109 |
+
background: oklch(75% 0.1 55);
|
110 |
}
|
111 |
|
112 |
.handle-name {
|
|
|
336 |
}
|
337 |
}
|
338 |
|
339 |
+
@keyframes active-node-gradient-animation {
|
340 |
+
0% {
|
341 |
+
background-position-x: 100%;
|
342 |
+
}
|
343 |
+
|
344 |
+
50% {
|
345 |
+
background-position-x: 0%;
|
346 |
+
}
|
347 |
+
|
348 |
+
100% {
|
349 |
+
background-position-x: 100%;
|
350 |
+
}
|
351 |
+
}
|
352 |
+
|
353 |
.react-flow__edge.selected path.react-flow__edge-path {
|
354 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
355 |
outline-offset: 10px;
|
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx
CHANGED
@@ -72,7 +72,7 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
|
|
72 |
>
|
73 |
<div className="lynxkite-node" style={props.nodeStyle}>
|
74 |
<div
|
75 |
-
className={`title bg-primary ${data.
|
76 |
onClick={titleClicked}
|
77 |
>
|
78 |
{data.title}
|
|
|
72 |
>
|
73 |
<div className="lynxkite-node" style={props.nodeStyle}>
|
74 |
<div
|
75 |
+
className={`title bg-primary ${data.status}`}
|
76 |
onClick={titleClicked}
|
77 |
>
|
78 |
{data.title}
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3 |
import json
|
4 |
from typing import Optional
|
5 |
import dataclasses
|
|
|
6 |
import os
|
7 |
import pycrdt
|
8 |
import pydantic
|
@@ -21,12 +22,18 @@ class Position(BaseConfig):
|
|
21 |
y: float
|
22 |
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
class WorkspaceNodeData(BaseConfig):
|
25 |
title: str
|
26 |
params: dict
|
27 |
display: Optional[object] = None
|
28 |
error: Optional[str] = None
|
29 |
-
|
30 |
# Also contains a "meta" field when going out.
|
31 |
# This is ignored when coming back from the frontend.
|
32 |
|
@@ -40,16 +47,22 @@ class WorkspaceNode(BaseConfig):
|
|
40 |
position: Position
|
41 |
_crdt: pycrdt.Map
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
def publish_result(self, result: ops.Result):
|
44 |
"""Sends the result to the frontend. Call this in an executor when the result is available."""
|
45 |
self.data.display = result.display
|
46 |
self.data.error = result.error
|
47 |
-
self.data.
|
48 |
if hasattr(self, "_crdt"):
|
49 |
with self._crdt.doc.transaction():
|
50 |
self._crdt["data"]["display"] = result.display
|
51 |
self._crdt["data"]["error"] = result.error
|
52 |
-
self._crdt["data"]["
|
53 |
|
54 |
def publish_error(self, error: Exception | str):
|
55 |
self.publish_result(ops.Result(error=str(error)))
|
|
|
3 |
import json
|
4 |
from typing import Optional
|
5 |
import dataclasses
|
6 |
+
import enum
|
7 |
import os
|
8 |
import pycrdt
|
9 |
import pydantic
|
|
|
22 |
y: float
|
23 |
|
24 |
|
25 |
+
class NodeStatus(str, enum.Enum):
|
26 |
+
planned = "planned"
|
27 |
+
active = "active"
|
28 |
+
done = "done"
|
29 |
+
|
30 |
+
|
31 |
class WorkspaceNodeData(BaseConfig):
|
32 |
title: str
|
33 |
params: dict
|
34 |
display: Optional[object] = None
|
35 |
error: Optional[str] = None
|
36 |
+
status: NodeStatus = NodeStatus.done
|
37 |
# Also contains a "meta" field when going out.
|
38 |
# This is ignored when coming back from the frontend.
|
39 |
|
|
|
47 |
position: Position
|
48 |
_crdt: pycrdt.Map
|
49 |
|
50 |
+
def publish_started(self):
|
51 |
+
"""Notifies the frontend that work has started on this node."""
|
52 |
+
self.data.status = NodeStatus.active
|
53 |
+
if hasattr(self, "_crdt"):
|
54 |
+
self._crdt["data"]["status"] = NodeStatus.active
|
55 |
+
|
56 |
def publish_result(self, result: ops.Result):
|
57 |
"""Sends the result to the frontend. Call this in an executor when the result is available."""
|
58 |
self.data.display = result.display
|
59 |
self.data.error = result.error
|
60 |
+
self.data.status = NodeStatus.done
|
61 |
if hasattr(self, "_crdt"):
|
62 |
with self._crdt.doc.transaction():
|
63 |
self._crdt["data"]["display"] = result.display
|
64 |
self._crdt["data"]["error"] = result.error
|
65 |
+
self._crdt["data"]["status"] = NodeStatus.done
|
66 |
|
67 |
def publish_error(self, error: Exception | str):
|
68 |
self.publish_result(ops.Result(error=str(error)))
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py
CHANGED
@@ -164,6 +164,7 @@ async def execute(ws):
|
|
164 |
node.publish_error("Operation not found in catalog")
|
165 |
failed += 1
|
166 |
continue
|
|
|
167 |
try:
|
168 |
# Convert inputs types to match operation signature.
|
169 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|
|
|
164 |
node.publish_error("Operation not found in catalog")
|
165 |
failed += 1
|
166 |
continue
|
167 |
+
node.publish_started()
|
168 |
try:
|
169 |
# Convert inputs types to match operation signature.
|
170 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|