Spaces:
Running
Running
Some backend execution. Errors.
Browse files- .gitignore +11 -0
- main.py +0 -68
- run.sh +2 -0
- server/__init__.py +0 -0
- server/basic_ops.py +42 -0
- server/main.py +81 -0
- server/ops.py +53 -0
- web/.gitignore +0 -11
- web/src/LynxKiteFlow.svelte +15 -37
- web/src/LynxKiteNode.svelte +14 -2
- web/src/NodeSearch.svelte +1 -1
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Editor directories and files
|
| 2 |
+
.vscode/*
|
| 3 |
+
!.vscode/extensions.json
|
| 4 |
+
.idea
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.suo
|
| 7 |
+
*.ntvs*
|
| 8 |
+
*.njsproj
|
| 9 |
+
*.sln
|
| 10 |
+
*.sw?
|
| 11 |
+
__pycache__
|
main.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
from typing import Union
|
| 2 |
-
import fastapi
|
| 3 |
-
import pydantic
|
| 4 |
-
import networkx as nx
|
| 5 |
-
|
| 6 |
-
class Position(pydantic.BaseModel):
|
| 7 |
-
x: float
|
| 8 |
-
y: float
|
| 9 |
-
|
| 10 |
-
class WorkspaceNodeData(pydantic.BaseModel):
|
| 11 |
-
title: str
|
| 12 |
-
params: dict
|
| 13 |
-
|
| 14 |
-
class WorkspaceNode(pydantic.BaseModel):
|
| 15 |
-
id: str
|
| 16 |
-
type: str
|
| 17 |
-
data: WorkspaceNodeData
|
| 18 |
-
position: Position
|
| 19 |
-
|
| 20 |
-
class WorkspaceEdge(pydantic.BaseModel):
|
| 21 |
-
id: str
|
| 22 |
-
source: str
|
| 23 |
-
target: str
|
| 24 |
-
|
| 25 |
-
class Workspace(pydantic.BaseModel):
|
| 26 |
-
nodes: list[WorkspaceNode]
|
| 27 |
-
edges: list[WorkspaceEdge]
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
app = fastapi.FastAPI()
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@app.get("/")
|
| 34 |
-
def read_root():
|
| 35 |
-
return {"Hello": "World"}
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
@app.get("/items/{item_id}")
|
| 39 |
-
def read_item(item_id: int, q: Union[str, None] = None):
|
| 40 |
-
return {"item_id": item_id, "q": q}
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
@app.post("/api/save")
|
| 44 |
-
def save(ws: Workspace):
|
| 45 |
-
print(ws)
|
| 46 |
-
G = nx.scale_free_graph(4)
|
| 47 |
-
return {'graph':{
|
| 48 |
-
'attributes': {
|
| 49 |
-
'name': 'My Graph'
|
| 50 |
-
},
|
| 51 |
-
'options': {
|
| 52 |
-
'allowSelfLoops': True,
|
| 53 |
-
'multi': False,
|
| 54 |
-
'type': 'mixed'
|
| 55 |
-
},
|
| 56 |
-
'nodes': [
|
| 57 |
-
{'key': 'Thomas'},
|
| 58 |
-
{'key': 'Eric'}
|
| 59 |
-
],
|
| 60 |
-
'edges': [
|
| 61 |
-
{
|
| 62 |
-
'key': 'T->E',
|
| 63 |
-
'source': 'Thomas',
|
| 64 |
-
'target': 'Eric',
|
| 65 |
-
}
|
| 66 |
-
]
|
| 67 |
-
}}
|
| 68 |
-
return {"graph": list(nx.to_edgelist(G))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash -xue
|
| 2 |
+
uvicorn server.main:app --reload
|
server/__init__.py
ADDED
|
File without changes
|
server/basic_ops.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''Some operations. To be split into separate files when we have more.'''
|
| 2 |
+
from . import ops
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import networkx as nx
|
| 5 |
+
|
| 6 |
+
@ops.op("Import Parquet")
|
| 7 |
+
def import_parquet(*, filename: str):
|
| 8 |
+
'''Imports a parquet file.'''
|
| 9 |
+
return pd.read_parquet(filename)
|
| 10 |
+
|
| 11 |
+
@ops.op("Create scale-free graph")
|
| 12 |
+
def create_scale_free_graph(*, nodes: int):
|
| 13 |
+
'''Creates a scale-free graph with the given number of nodes.'''
|
| 14 |
+
return nx.scale_free_graph(nodes)
|
| 15 |
+
|
| 16 |
+
@ops.op("Compute PageRank")
|
| 17 |
+
def compute_pagerank(graph, *, damping: 0.85, iterations: 3):
|
| 18 |
+
return nx.pagerank(graph)
|
| 19 |
+
|
| 20 |
+
@ops.op("Visualize graph")
|
| 21 |
+
def visualize_graph(graph) -> 'graphviz':
|
| 22 |
+
return {'graph':{
|
| 23 |
+
'attributes': {
|
| 24 |
+
'name': 'My Graph'
|
| 25 |
+
},
|
| 26 |
+
'options': {
|
| 27 |
+
'allowSelfLoops': True,
|
| 28 |
+
'multi': False,
|
| 29 |
+
'type': 'mixed'
|
| 30 |
+
},
|
| 31 |
+
'nodes': [
|
| 32 |
+
{'key': 'Thomas'},
|
| 33 |
+
{'key': 'Eric'}
|
| 34 |
+
],
|
| 35 |
+
'edges': [
|
| 36 |
+
{
|
| 37 |
+
'key': 'T->E',
|
| 38 |
+
'source': 'Thomas',
|
| 39 |
+
'target': 'Eric',
|
| 40 |
+
}
|
| 41 |
+
]
|
| 42 |
+
}}
|
server/main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
import fastapi
|
| 3 |
+
import pydantic
|
| 4 |
+
from . import ops
|
| 5 |
+
from . import basic_ops
|
| 6 |
+
|
| 7 |
+
class BaseConfig(pydantic.BaseModel):
|
| 8 |
+
model_config = pydantic.ConfigDict(
|
| 9 |
+
extra='allow',
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
class Position(BaseConfig):
|
| 13 |
+
x: float
|
| 14 |
+
y: float
|
| 15 |
+
|
| 16 |
+
class WorkspaceNodeData(BaseConfig):
|
| 17 |
+
title: str
|
| 18 |
+
params: dict
|
| 19 |
+
display: Optional[object] = None
|
| 20 |
+
error: Optional[str] = None
|
| 21 |
+
|
| 22 |
+
class WorkspaceNode(BaseConfig):
|
| 23 |
+
id: str
|
| 24 |
+
type: str
|
| 25 |
+
data: WorkspaceNodeData
|
| 26 |
+
position: Position
|
| 27 |
+
|
| 28 |
+
class WorkspaceEdge(BaseConfig):
|
| 29 |
+
id: str
|
| 30 |
+
source: str
|
| 31 |
+
target: str
|
| 32 |
+
|
| 33 |
+
class Workspace(BaseConfig):
|
| 34 |
+
nodes: list[WorkspaceNode]
|
| 35 |
+
edges: list[WorkspaceEdge]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
app = fastapi.FastAPI()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.get("/api/catalog")
|
| 42 |
+
def get_catalog():
|
| 43 |
+
return [
|
| 44 |
+
{
|
| 45 |
+
'type': op.type,
|
| 46 |
+
'data': { 'title': op.name, 'params': op.params },
|
| 47 |
+
'targetPosition': 'left' if op.inputs else None,
|
| 48 |
+
'sourcePosition': 'right' if op.outputs else None,
|
| 49 |
+
}
|
| 50 |
+
for op in ops.ALL_OPS.values()]
|
| 51 |
+
|
| 52 |
+
def execute(ws):
|
| 53 |
+
nodes = ws.nodes
|
| 54 |
+
outputs = {}
|
| 55 |
+
failed = 0
|
| 56 |
+
while len(outputs) + failed < len(nodes):
|
| 57 |
+
for node in nodes:
|
| 58 |
+
if node.id in outputs:
|
| 59 |
+
continue
|
| 60 |
+
inputs = [edge.source for edge in ws.edges if edge.target == node.id]
|
| 61 |
+
if all(input in outputs for input in inputs):
|
| 62 |
+
inputs = [outputs[input] for input in inputs]
|
| 63 |
+
data = node.data
|
| 64 |
+
op = ops.ALL_OPS[data.title]
|
| 65 |
+
try:
|
| 66 |
+
output = op(*inputs, **data.params)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
data.error = str(e)
|
| 69 |
+
failed += 1
|
| 70 |
+
continue
|
| 71 |
+
outputs[node.id] = output
|
| 72 |
+
if op.type == 'graphviz':
|
| 73 |
+
data.graph = output
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@app.post("/api/save")
|
| 77 |
+
def save(ws: Workspace):
|
| 78 |
+
print(ws)
|
| 79 |
+
execute(ws)
|
| 80 |
+
print('exec done', ws)
|
| 81 |
+
return ws
|
server/ops.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''API for implementing LynxKite operations.'''
|
| 2 |
+
import dataclasses
|
| 3 |
+
import inspect
|
| 4 |
+
|
| 5 |
+
ALL_OPS = {}
|
| 6 |
+
|
| 7 |
+
@dataclasses.dataclass
|
| 8 |
+
class Op:
|
| 9 |
+
func: callable
|
| 10 |
+
name: str
|
| 11 |
+
params: dict
|
| 12 |
+
inputs: dict
|
| 13 |
+
outputs: dict
|
| 14 |
+
type: str
|
| 15 |
+
|
| 16 |
+
def __call__(self, *inputs, **params):
|
| 17 |
+
# Do conversions here.
|
| 18 |
+
res = self.func(*inputs, **params)
|
| 19 |
+
return res
|
| 20 |
+
|
| 21 |
+
@dataclasses.dataclass
|
| 22 |
+
class EdgeDefinition:
|
| 23 |
+
df: str
|
| 24 |
+
source_column: str
|
| 25 |
+
target_column: str
|
| 26 |
+
source_table: str
|
| 27 |
+
target_table: str
|
| 28 |
+
source_key: str
|
| 29 |
+
target_key: str
|
| 30 |
+
|
| 31 |
+
@dataclasses.dataclass
|
| 32 |
+
class Bundle:
|
| 33 |
+
dfs: dict
|
| 34 |
+
edges: list[EdgeDefinition]
|
| 35 |
+
|
| 36 |
+
def op(name):
|
| 37 |
+
'''Decorator for defining an operation.'''
|
| 38 |
+
def decorator(func):
|
| 39 |
+
type = func.__annotations__.get('return') or 'basic'
|
| 40 |
+
sig = inspect.signature(func)
|
| 41 |
+
# Positional arguments are inputs.
|
| 42 |
+
inputs = {
|
| 43 |
+
name: param.annotation
|
| 44 |
+
for name, param in sig.parameters.items()
|
| 45 |
+
if param.kind != param.KEYWORD_ONLY}
|
| 46 |
+
params = {
|
| 47 |
+
name: param.default if param.default is not inspect._empty else None
|
| 48 |
+
for name, param in sig.parameters.items()
|
| 49 |
+
if param.kind == param.KEYWORD_ONLY}
|
| 50 |
+
op = Op(func, name, params=params, inputs=inputs, outputs={'output': 'yes'}, type=type)
|
| 51 |
+
ALL_OPS[name] = op
|
| 52 |
+
return func
|
| 53 |
+
return decorator
|
web/.gitignore
CHANGED
|
@@ -11,14 +11,3 @@ node_modules
|
|
| 11 |
dist
|
| 12 |
dist-ssr
|
| 13 |
*.local
|
| 14 |
-
|
| 15 |
-
# Editor directories and files
|
| 16 |
-
.vscode/*
|
| 17 |
-
!.vscode/extensions.json
|
| 18 |
-
.idea
|
| 19 |
-
.DS_Store
|
| 20 |
-
*.suo
|
| 21 |
-
*.ntvs*
|
| 22 |
-
*.njsproj
|
| 23 |
-
*.sln
|
| 24 |
-
*.sw?
|
|
|
|
| 11 |
dist
|
| 12 |
dist-ssr
|
| 13 |
*.local
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/LynxKiteFlow.svelte
CHANGED
|
@@ -37,7 +37,7 @@
|
|
| 37 |
id: '3',
|
| 38 |
type: 'basic',
|
| 39 |
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
| 40 |
-
position: { x: -
|
| 41 |
sourcePosition: Position.Right,
|
| 42 |
},
|
| 43 |
{
|
|
@@ -54,17 +54,13 @@
|
|
| 54 |
id: '3-1',
|
| 55 |
source: '3',
|
| 56 |
target: '1',
|
| 57 |
-
markerEnd: {
|
| 58 |
-
type: MarkerType.ArrowClosed,
|
| 59 |
-
},
|
| 60 |
},
|
| 61 |
{
|
| 62 |
id: '3-4',
|
| 63 |
source: '1',
|
| 64 |
target: '4',
|
| 65 |
-
markerEnd: {
|
| 66 |
-
type: MarkerType.ArrowClosed,
|
| 67 |
-
},
|
| 68 |
},
|
| 69 |
]);
|
| 70 |
|
|
@@ -105,30 +101,13 @@
|
|
| 105 |
});
|
| 106 |
closeNodeSearch();
|
| 107 |
}
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
{
|
| 116 |
-
type: 'basic',
|
| 117 |
-
data: { title: 'Export Parquet', params: { filename: '/tmp/x.parquet' } },
|
| 118 |
-
targetPosition: Position.Left,
|
| 119 |
-
},
|
| 120 |
-
{
|
| 121 |
-
type: 'graphviz',
|
| 122 |
-
data: { title: 'Visualize graph', params: {} },
|
| 123 |
-
targetPosition: Position.Left,
|
| 124 |
-
},
|
| 125 |
-
{
|
| 126 |
-
type: 'basic',
|
| 127 |
-
data: { title: 'Compute PageRank', params: { damping: 0.85, iterations: 3 } },
|
| 128 |
-
sourcePosition: Position.Right,
|
| 129 |
-
targetPosition: Position.Left,
|
| 130 |
-
},
|
| 131 |
-
];
|
| 132 |
|
| 133 |
let nodeSearchPos: XYPosition | undefined = undefined;
|
| 134 |
|
|
@@ -143,6 +122,7 @@
|
|
| 143 |
}
|
| 144 |
const ws = JSON.stringify(g);
|
| 145 |
if (ws === backendWorkspace) return;
|
|
|
|
| 146 |
backendWorkspace = ws;
|
| 147 |
const res = await fetch('/api/save', {
|
| 148 |
method: 'POST',
|
|
@@ -152,9 +132,8 @@
|
|
| 152 |
body: JSON.stringify(g),
|
| 153 |
});
|
| 154 |
const j = await res.json();
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
nodes.set(g.nodes);
|
| 158 |
});
|
| 159 |
</script>
|
| 160 |
|
|
@@ -165,10 +144,9 @@
|
|
| 165 |
maxZoom={1.5}
|
| 166 |
minZoom={0.3}
|
| 167 |
>
|
| 168 |
-
<Background />
|
| 169 |
<Controls />
|
| 170 |
-
<Background />
|
| 171 |
<MiniMap />
|
| 172 |
-
{#if nodeSearchPos}<NodeSearch boxes={boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
| 173 |
</SvelteFlow>
|
| 174 |
</div>
|
|
|
|
| 37 |
id: '3',
|
| 38 |
type: 'basic',
|
| 39 |
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
| 40 |
+
position: { x: -400, y: 0 },
|
| 41 |
sourcePosition: Position.Right,
|
| 42 |
},
|
| 43 |
{
|
|
|
|
| 54 |
id: '3-1',
|
| 55 |
source: '3',
|
| 56 |
target: '1',
|
| 57 |
+
// markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
|
|
|
| 58 |
},
|
| 59 |
{
|
| 60 |
id: '3-4',
|
| 61 |
source: '1',
|
| 62 |
target: '4',
|
| 63 |
+
// markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
|
|
|
| 64 |
},
|
| 65 |
]);
|
| 66 |
|
|
|
|
| 101 |
});
|
| 102 |
closeNodeSearch();
|
| 103 |
}
|
| 104 |
+
const boxes = writable([]);
|
| 105 |
+
async function getBoxes() {
|
| 106 |
+
const res = await fetch('/api/catalog');
|
| 107 |
+
const j = await res.json();
|
| 108 |
+
boxes.set(j);
|
| 109 |
+
}
|
| 110 |
+
getBoxes();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
let nodeSearchPos: XYPosition | undefined = undefined;
|
| 113 |
|
|
|
|
| 122 |
}
|
| 123 |
const ws = JSON.stringify(g);
|
| 124 |
if (ws === backendWorkspace) return;
|
| 125 |
+
console.log('current vs backend', '\n' + ws, '\n' + backendWorkspace);
|
| 126 |
backendWorkspace = ws;
|
| 127 |
const res = await fetch('/api/save', {
|
| 128 |
method: 'POST',
|
|
|
|
| 132 |
body: JSON.stringify(g),
|
| 133 |
});
|
| 134 |
const j = await res.json();
|
| 135 |
+
backendWorkspace = JSON.stringify(j);
|
| 136 |
+
nodes.set(j.nodes);
|
|
|
|
| 137 |
});
|
| 138 |
</script>
|
| 139 |
|
|
|
|
| 144 |
maxZoom={1.5}
|
| 145 |
minZoom={0.3}
|
| 146 |
>
|
| 147 |
+
<Background patternColor="#39bcf3" />
|
| 148 |
<Controls />
|
|
|
|
| 149 |
<MiniMap />
|
| 150 |
+
{#if nodeSearchPos}<NodeSearch boxes={$boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
| 151 |
</SvelteFlow>
|
| 152 |
</div>
|
web/src/LynxKiteNode.svelte
CHANGED
|
@@ -28,8 +28,12 @@
|
|
| 28 |
<div class="lynxkite-node">
|
| 29 |
<div class="title" on:click={titleClicked}>
|
| 30 |
{data.title}
|
|
|
|
| 31 |
</div>
|
| 32 |
{#if expanded}
|
|
|
|
|
|
|
|
|
|
| 33 |
<slot />
|
| 34 |
{/if}
|
| 35 |
{#if sourcePosition}
|
|
@@ -42,8 +46,18 @@
|
|
| 42 |
</div>
|
| 43 |
|
| 44 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
.node-container {
|
| 46 |
padding: 8px;
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
.lynxkite-node {
|
| 49 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
|
@@ -53,7 +67,5 @@
|
|
| 53 |
background: #ff8800;
|
| 54 |
font-weight: bold;
|
| 55 |
padding: 8px;
|
| 56 |
-
min-width: 170px;
|
| 57 |
-
max-width: 300px;
|
| 58 |
}
|
| 59 |
</style>
|
|
|
|
| 28 |
<div class="lynxkite-node">
|
| 29 |
<div class="title" on:click={titleClicked}>
|
| 30 |
{data.title}
|
| 31 |
+
{#if data.error}<span class="error-sign">⚠️</span>{/if}
|
| 32 |
</div>
|
| 33 |
{#if expanded}
|
| 34 |
+
{#if data.error}
|
| 35 |
+
<div class="error">{data.error}</div>
|
| 36 |
+
{/if}
|
| 37 |
<slot />
|
| 38 |
{/if}
|
| 39 |
{#if sourcePosition}
|
|
|
|
| 46 |
</div>
|
| 47 |
|
| 48 |
<style>
|
| 49 |
+
.error {
|
| 50 |
+
background: #ffdddd;
|
| 51 |
+
padding: 8px;
|
| 52 |
+
font-size: 12px;
|
| 53 |
+
}
|
| 54 |
+
.error-sign {
|
| 55 |
+
float: right;
|
| 56 |
+
}
|
| 57 |
.node-container {
|
| 58 |
padding: 8px;
|
| 59 |
+
min-width: 170px;
|
| 60 |
+
max-width: 300px;
|
| 61 |
}
|
| 62 |
.lynxkite-node {
|
| 63 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
|
|
|
| 67 |
background: #ff8800;
|
| 68 |
font-weight: bold;
|
| 69 |
padding: 8px;
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
</style>
|
web/src/NodeSearch.svelte
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
let hits = [];
|
| 9 |
let selectedIndex = 0;
|
| 10 |
onMount(() => searchBox.focus());
|
| 11 |
-
|
| 12 |
keys: ['data.title']
|
| 13 |
})
|
| 14 |
function onInput() {
|
|
|
|
| 8 |
let hits = [];
|
| 9 |
let selectedIndex = 0;
|
| 10 |
onMount(() => searchBox.focus());
|
| 11 |
+
$: fuse = new Fuse(boxes, {
|
| 12 |
keys: ['data.title']
|
| 13 |
})
|
| 14 |
function onInput() {
|