Compare commits
7 Commits
eb8d6a99a2
...
ResolvingP
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dc7687b25 | |||
| f08ae4a90b | |||
| b48aaf4621 | |||
| 2c5fe004f5 | |||
| 9cf0e5e26a | |||
| 67abb45804 | |||
| 5820efb7f1 |
@@ -1,4 +1,4 @@
|
|||||||
from core.utils import get_user_id, get_unique_id
|
from core.utils import get_user_id
|
||||||
|
|
||||||
|
|
||||||
class BaseComponent:
|
class BaseComponent:
|
||||||
@@ -51,12 +51,3 @@ class BaseComponentSingleton(BaseComponent):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create_component_id(cls, session):
|
def create_component_id(cls, session):
|
||||||
return f"{cls.COMPONENT_INSTANCE_ID}{session['user_id']}"
|
return f"{cls.COMPONENT_INSTANCE_ID}{session['user_id']}"
|
||||||
|
|
||||||
|
|
||||||
class BaseComponentMultipleInstance(BaseComponent):
|
|
||||||
COMPONENT_INSTANCE_ID = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_component_id(cls, session):
|
|
||||||
component_id = cls.COMPONENT_INSTANCE_ID or cls.__name__
|
|
||||||
return get_unique_id(f"{component_id}{session['user_id']}")
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from fasthtml.components import Div, sse_message
|
||||||
|
from fasthtml.core import EventStream
|
||||||
from fasthtml.fastapp import fast_app
|
from fasthtml.fastapp import fast_app
|
||||||
from starlette.datastructures import UploadFile
|
from starlette.datastructures import UploadFile
|
||||||
|
|
||||||
@@ -138,6 +141,12 @@ def post(session, _id: str, state: str, args: str = None):
|
|||||||
return instance.manage_state_changed(state, args)
|
return instance.manage_state_changed(state, args)
|
||||||
|
|
||||||
|
|
||||||
|
@rt(Routes.YieldRow)
|
||||||
|
async def get(session, _id: str):
|
||||||
|
logger.debug(f"Entering {Routes.YieldRow} with args {_id=}")
|
||||||
|
instance = InstanceManager.get(session, _id)
|
||||||
|
return EventStream(instance.mk_body_content_sse())
|
||||||
|
|
||||||
@rt(Routes.GetPage)
|
@rt(Routes.GetPage)
|
||||||
def get(session, _id: str, page_index: int):
|
def get(session, _id: str, page_index: int):
|
||||||
logger.debug(f"Entering {Routes.GetPage} with args {_id=}, {page_index=}")
|
logger.debug(f"Entering {Routes.GetPage} with args {_id=}, {page_index=}")
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ class DataGrid(BaseComponent):
|
|||||||
id=f"scb_{self._id}",
|
id=f"scb_{self._id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@timed
|
||||||
def mk_table(self, oob=False):
|
def mk_table(self, oob=False):
|
||||||
htmx_extra_params = {
|
htmx_extra_params = {
|
||||||
"hx-on::before-settle": f"onAfterSettle('{self._id}', event);",
|
"hx-on::before-settle": f"onAfterSettle('{self._id}', event);",
|
||||||
|
|||||||
@@ -118,6 +118,38 @@ class DataGridCommandManager(BaseCommandManager):
|
|||||||
"data_tooltip": tooltip_msg,
|
"data_tooltip": tooltip_msg,
|
||||||
"cls": self.merge_class(cls, "mmt-tooltip")
|
"cls": self.merge_class(cls, "mmt-tooltip")
|
||||||
}
|
}
|
||||||
|
#
|
||||||
|
# @staticmethod
|
||||||
|
# def merge(*items):
|
||||||
|
# """
|
||||||
|
# Merges multiple dictionaries into a single dictionary by combining their key-value pairs.
|
||||||
|
# If a key exists in multiple dictionaries and its value is a string, the values are concatenated.
|
||||||
|
# If the key's value is not a string, an error is raised.
|
||||||
|
#
|
||||||
|
# :param items: dictionaries to be merged. If all items are None, None is returned.
|
||||||
|
# :return: A single dictionary containing the merged key-value pairs from all input dictionaries.
|
||||||
|
# :raises NotImplementedError: If a key's value is not a string and exists in multiple input dictionaries.
|
||||||
|
# """
|
||||||
|
# if all(item is None for item in items):
|
||||||
|
# return None
|
||||||
|
#
|
||||||
|
# res = {}
|
||||||
|
# for item in [item for item in items if item is not None]:
|
||||||
|
#
|
||||||
|
# for key, value in item.items():
|
||||||
|
# if not key in res:
|
||||||
|
# res[key] = value
|
||||||
|
# else:
|
||||||
|
# if isinstance(res[key], str):
|
||||||
|
# res[key] += " " + value
|
||||||
|
# else:
|
||||||
|
# raise NotImplementedError("")
|
||||||
|
#
|
||||||
|
# return res
|
||||||
|
#
|
||||||
|
# @staticmethod
|
||||||
|
# def merge_class(cls1, cls2):
|
||||||
|
# return (cls1 + " " + cls2) if cls2 else cls1
|
||||||
|
|
||||||
|
|
||||||
class FilterAllCommands(BaseCommandManager):
|
class FilterAllCommands(BaseCommandManager):
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Routes:
|
|||||||
UpdateView = "/update_view"
|
UpdateView = "/update_view"
|
||||||
ShowFooterMenu = "/show_footer_menu"
|
ShowFooterMenu = "/show_footer_menu"
|
||||||
UpdateState = "/update_state"
|
UpdateState = "/update_state"
|
||||||
|
YieldRow = "/yield-row"
|
||||||
GetPage = "/page"
|
GetPage = "/page"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class JsonViewerHelper:
|
|||||||
class JsonViewer(BaseComponent):
|
class JsonViewer(BaseComponent):
|
||||||
def __init__(self, session, _id, owner, user_id, data, hooks=None, key=None, boundaries=None):
|
def __init__(self, session, _id, owner, user_id, data, hooks=None, key=None, boundaries=None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id)
|
||||||
self._key = key # for comparison between two jsonviewer components
|
self._key = key
|
||||||
self._owner = owner # debugger component
|
self._owner = owner # debugger component
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.data = data
|
self.data = data
|
||||||
@@ -88,10 +88,6 @@ class JsonViewer(BaseComponent):
|
|||||||
|
|
||||||
self._helper = JsonViewerHelper()
|
self._helper = JsonViewerHelper()
|
||||||
|
|
||||||
def set_data(self, data):
|
|
||||||
self.data = data
|
|
||||||
self.node = self._create_node(None, data)
|
|
||||||
|
|
||||||
def set_node_folding(self, node_id, folding):
|
def set_node_folding(self, node_id, folding):
|
||||||
if folding == self._folding_mode:
|
if folding == self._folding_mode:
|
||||||
self._nodes_to_track.remove(node_id)
|
self._nodes_to_track.remove(node_id)
|
||||||
@@ -315,6 +311,8 @@ class JsonViewer(BaseComponent):
|
|||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self._key) if self._key is not None else super().__hash__()
|
return hash(self._key) if self._key is not None else super().__hash__()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_quotes(value: str):
|
def add_quotes(value: str):
|
||||||
if '"' in value and "'" in value:
|
if '"' in value and "'" in value:
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from fasthtml.fastapp import fast_app
|
|
||||||
|
|
||||||
from components.entryselector.constants import Routes
|
|
||||||
from core.instance_manager import debug_session, InstanceManager
|
|
||||||
|
|
||||||
logger = logging.getLogger("EntrySelectorApp")
|
|
||||||
|
|
||||||
repositories_app, rt = fast_app()
|
|
||||||
|
|
||||||
|
|
||||||
@rt(Routes.Select)
|
|
||||||
def get(session, _id: str, entry: str):
|
|
||||||
logger.debug(f"Entering {Routes.Select} with args {debug_session(session)}, {_id=}, {entry=}")
|
|
||||||
instance = InstanceManager.get(session, _id)
|
|
||||||
to_update = instance.select_entry(entry)
|
|
||||||
|
|
||||||
res = [instance]
|
|
||||||
if res is None:
|
|
||||||
return instance
|
|
||||||
if isinstance(to_update, (list, tuple)):
|
|
||||||
res.extend(to_update)
|
|
||||||
else:
|
|
||||||
res.append(to_update)
|
|
||||||
return tuple(res)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.es-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.es-entry {
|
|
||||||
border: 2px solid var(--color-base-300);
|
|
||||||
padding: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block; /* Ensure entries align horizontally if needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
.es-entry-selected {
|
|
||||||
border: 2px solid var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.es-entry:hover {
|
|
||||||
background-color: var(--color-base-300);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from components.BaseCommandManager import BaseCommandManager
|
|
||||||
from components.entryselector.constants import Routes, ROUTE_ROOT
|
|
||||||
|
|
||||||
|
|
||||||
class EntrySelectorCommandManager(BaseCommandManager):
|
|
||||||
def __init__(self, owner):
|
|
||||||
super().__init__(owner)
|
|
||||||
|
|
||||||
def select_entry(self, entry):
|
|
||||||
return {
|
|
||||||
"hx-get": f"{ROUTE_ROOT}{Routes.Select}",
|
|
||||||
"hx-target": f"#{self._id}",
|
|
||||||
"hx-swap": "outerHTML",
|
|
||||||
"hx-vals": f'{{"_id": "{self._id}", "entry": "{entry}"}}',
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from fasthtml.components import *
|
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponentMultipleInstance
|
|
||||||
from components.entryselector.commands import EntrySelectorCommandManager
|
|
||||||
|
|
||||||
logger = logging.getLogger("EntrySelector")
|
|
||||||
|
|
||||||
|
|
||||||
class EntrySelector(BaseComponentMultipleInstance):
|
|
||||||
def __init__(self, session, _id, owner, data=None, hooks=None, key=None, boundaries=None):
|
|
||||||
super().__init__(session, _id)
|
|
||||||
self._key = key
|
|
||||||
self._owner = owner # debugger component
|
|
||||||
self.data = data
|
|
||||||
self.selected = None
|
|
||||||
self.hooks = hooks
|
|
||||||
self._boundaries = boundaries if boundaries else {"width": "300"}
|
|
||||||
self._commands = EntrySelectorCommandManager(self)
|
|
||||||
|
|
||||||
def set_data(self, data):
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def set_selected(self, selected):
|
|
||||||
if selected is None:
|
|
||||||
self.selected = None
|
|
||||||
else:
|
|
||||||
self.selected = int(selected)
|
|
||||||
|
|
||||||
def set_boundaries(self, boundaries):
|
|
||||||
self._boundaries = boundaries
|
|
||||||
|
|
||||||
def select_entry(self, entry):
|
|
||||||
logger.debug(f"Selecting entry {entry}")
|
|
||||||
self.set_selected(entry)
|
|
||||||
if self.hooks is not None and (on_entry_selected := self.hooks.get("on_entry_selected", None)) is not None:
|
|
||||||
return on_entry_selected(entry)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _mk_content(self):
|
|
||||||
if not self.data:
|
|
||||||
return [Div("no entry")]
|
|
||||||
|
|
||||||
return [Div(index,
|
|
||||||
**self._commands.select_entry(index),
|
|
||||||
cls=f"es-entry {'es-entry-selected' if index == self.selected else ''}")
|
|
||||||
for index in range(self.data)]
|
|
||||||
|
|
||||||
def __ft__(self):
|
|
||||||
return Div(
|
|
||||||
*self._mk_content(),
|
|
||||||
cls="flex es-container",
|
|
||||||
id=f"{self._id}",
|
|
||||||
)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ROUTE_ROOT = "/es" # for EntrySelector
|
|
||||||
|
|
||||||
|
|
||||||
class Routes:
|
|
||||||
Select = "/select"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from fasthtml.fastapp import fast_app
|
|
||||||
|
|
||||||
from components.jsonviewer.constants import Routes
|
|
||||||
from core.instance_manager import debug_session, InstanceManager
|
|
||||||
|
|
||||||
jsonviwer_app, rt = fast_app()
|
|
||||||
|
|
||||||
logger = logging.getLogger("JsonViewer")
|
|
||||||
|
|
||||||
|
|
||||||
@rt(Routes.Fold)
|
|
||||||
def post(session, _id: str, node_id: str, folding: str):
|
|
||||||
logger.debug(f"Entering {Routes.Fold} with args {debug_session(session)}, {_id=}, {node_id=}, {folding=}")
|
|
||||||
instance = InstanceManager.get(session, _id)
|
|
||||||
instance.set_node_folding(node_id, folding)
|
|
||||||
return instance.render_node(node_id)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from fastcore.basics import NotStr
|
|
||||||
|
|
||||||
# Fluent CaretRight20Filled
|
|
||||||
icon_collapsed = NotStr("""<svg name="collapsed" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20">
|
|
||||||
<g fill="none">
|
|
||||||
<path d="M7 14.204a1 1 0 0 0 1.628.778l4.723-3.815a1.5 1.5 0 0 0 0-2.334L8.628 5.02A1 1 0 0 0 7 5.797v8.407z" fill="currentColor">
|
|
||||||
</path>
|
|
||||||
</g>
|
|
||||||
</svg>""")
|
|
||||||
|
|
||||||
# Fluent CaretDown20Filled
|
|
||||||
icon_expanded = NotStr("""<svg name="expanded" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20">
|
|
||||||
<g fill="none">
|
|
||||||
<path d="M5.797 7a1 1 0 0 0-.778 1.628l3.814 4.723a1.5 1.5 0 0 0 2.334 0l3.815-4.723A1 1 0 0 0 14.204 7H5.797z" fill="currentColor">
|
|
||||||
</path>
|
|
||||||
</g>
|
|
||||||
</svg>""")
|
|
||||||
|
|
||||||
icon_class = NotStr("""
|
|
||||||
<svg name="expanded" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g fill="none" stroke="currentColor" stroke-width="1.5" >
|
|
||||||
<polygon points="5,2 2,8 8,8" />
|
|
||||||
<rect x="12" y="2" width="6" height="6"/>
|
|
||||||
<circle cx="5" cy="15" r="3" />
|
|
||||||
<polygon points="11.5,15 15,11.5 18.5,15 15,18.5" />
|
|
||||||
</g>
|
|
||||||
</svg>""")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from components.jsonviewer.constants import ROUTE_ROOT, Routes
|
|
||||||
|
|
||||||
|
|
||||||
class JsonViewerCommands:
|
|
||||||
def __init__(self, owner):
|
|
||||||
self._owner = owner
|
|
||||||
self._id = owner.get_id()
|
|
||||||
|
|
||||||
def fold(self, node_id: str, folding: str):
|
|
||||||
return {
|
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.Fold}",
|
|
||||||
"hx-target": f"#{node_id}",
|
|
||||||
"hx-swap": "outerHTML",
|
|
||||||
"hx-vals": f'{{"_id": "{self._id}", "node_id": "{node_id}", "folding": "{folding}"}}',
|
|
||||||
}
|
|
||||||
|
|
||||||
def open_digest(self, user_id, digest):
|
|
||||||
return {
|
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineDigest}",
|
|
||||||
"hx-target": f"#{self._owner.get_owner().tabs_manager.get_id()}",
|
|
||||||
"hx-swap": "outerHTML",
|
|
||||||
"hx-vals": f'{{"_id": "{self._id}", "user_id": "{user_id}", "digest": "{digest}"}}',
|
|
||||||
}
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any, Optional, Callable
|
|
||||||
|
|
||||||
from fasthtml.components import *
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponentMultipleInstance
|
|
||||||
from components.datagrid_new.components.DataGrid import DataGrid
|
|
||||||
from components.jsonviewer.assets.icons import icon_expanded, icon_collapsed, icon_class
|
|
||||||
from components.jsonviewer.commands import JsonViewerCommands
|
|
||||||
from components.jsonviewer.constants import NODES_KEYS_TO_NOT_EXPAND, NODE_OBJECT, INDENT_SIZE, MAX_TEXT_LENGTH
|
|
||||||
from components_helpers import apply_boundaries
|
|
||||||
from core.serializer import TAG_OBJECT
|
|
||||||
|
|
||||||
|
|
||||||
class FoldingMode:
|
|
||||||
COLLAPSE = "collapse"
|
|
||||||
EXPAND = "expand"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Node:
|
|
||||||
value: Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ValueNode(Node):
|
|
||||||
hint: str = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ListNode(Node):
|
|
||||||
node_id: str
|
|
||||||
level: int
|
|
||||||
children: list[Node] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DictNode(Node):
|
|
||||||
node_id: str
|
|
||||||
level: int
|
|
||||||
children: dict[str, Node] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeIdGenerator:
|
|
||||||
"""Manages unique node ID generation"""
|
|
||||||
|
|
||||||
def __init__(self, base_id: str):
|
|
||||||
self.base_id = base_id
|
|
||||||
self._counter = -1
|
|
||||||
|
|
||||||
def generate(self) -> str:
|
|
||||||
self._counter += 1
|
|
||||||
return f"{self.base_id}-{self._counter}"
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self._counter = -1
|
|
||||||
|
|
||||||
|
|
||||||
class FoldingManager:
|
|
||||||
"""Manages folding/unfolding state of nodes"""
|
|
||||||
|
|
||||||
# A little explanation on how the folding / unfolding work
|
|
||||||
# all the nodes are either fold or unfold... except when there are not !
|
|
||||||
# self._folding_mode keeps the current value (it's FoldingMode.COLLAPSE or FoldingMode.EXPAND
|
|
||||||
# self._nodes_to_track keeps track of the exceptions
|
|
||||||
# The idea is to minimize the memory usage
|
|
||||||
|
|
||||||
def __init__(self, default_mode: str = FoldingMode.COLLAPSE):
|
|
||||||
self._folding_mode = default_mode
|
|
||||||
self._nodes_to_track = set() # exceptions to the default mode
|
|
||||||
|
|
||||||
def set_folding_mode(self, mode: str):
|
|
||||||
"""Changes the global folding mode and clears exceptions"""
|
|
||||||
self._folding_mode = mode
|
|
||||||
self._nodes_to_track.clear()
|
|
||||||
|
|
||||||
def set_node_folding(self, node_id: str, folding: str):
|
|
||||||
"""Sets specific folding state for a node"""
|
|
||||||
if folding == self._folding_mode:
|
|
||||||
self._nodes_to_track.discard(node_id)
|
|
||||||
else:
|
|
||||||
self._nodes_to_track.add(node_id)
|
|
||||||
|
|
||||||
def must_expand(self, node: Node) -> Optional[bool]:
|
|
||||||
"""Determines if a node should be expanded"""
|
|
||||||
if not isinstance(node, (ListNode, DictNode)):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self._folding_mode == FoldingMode.COLLAPSE:
|
|
||||||
return node.node_id in self._nodes_to_track
|
|
||||||
else:
|
|
||||||
return node.node_id not in self._nodes_to_track
|
|
||||||
|
|
||||||
def get_folding_mode(self) -> str:
|
|
||||||
return self._folding_mode
|
|
||||||
|
|
||||||
def get_nodes_to_track(self) -> set[str]:
|
|
||||||
return self._nodes_to_track
|
|
||||||
|
|
||||||
|
|
||||||
class NodeFactory:
|
|
||||||
"""Factory for creating nodes from data"""
|
|
||||||
|
|
||||||
def __init__(self, id_generator: NodeIdGenerator, folding_manager: FoldingManager):
|
|
||||||
self.id_generator = id_generator
|
|
||||||
self.folding_manager = folding_manager
|
|
||||||
self._nodes_by_id = {}
|
|
||||||
|
|
||||||
def create_node(self, key: Any, data: Any, level: int = 0) -> Node:
|
|
||||||
"""Creates appropriate node type based on data"""
|
|
||||||
if isinstance(data, list):
|
|
||||||
return self._create_list_node(key, data, level)
|
|
||||||
elif isinstance(data, dict):
|
|
||||||
return self._create_dict_node(key, data, level)
|
|
||||||
else:
|
|
||||||
return self._create_value_node(key, data)
|
|
||||||
|
|
||||||
def _create_list_node(self, key: Any, data: list, level: int) -> ListNode:
|
|
||||||
node_id = self.id_generator.generate()
|
|
||||||
if level <= 1 and key not in NODES_KEYS_TO_NOT_EXPAND:
|
|
||||||
self.folding_manager._nodes_to_track.add(node_id)
|
|
||||||
|
|
||||||
node = ListNode(data, node_id, level)
|
|
||||||
self._nodes_by_id[node_id] = (key, node)
|
|
||||||
|
|
||||||
for index, item in enumerate(data):
|
|
||||||
node.children.append(self.create_node(index, item, level + 1))
|
|
||||||
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _create_dict_node(self, key: Any, data: dict, level: int) -> DictNode:
|
|
||||||
node_id = self.id_generator.generate()
|
|
||||||
if level <= 1 and key not in NODES_KEYS_TO_NOT_EXPAND:
|
|
||||||
self.folding_manager._nodes_to_track.add(node_id)
|
|
||||||
|
|
||||||
node = DictNode(data, node_id, level)
|
|
||||||
self._nodes_by_id[node_id] = (key, node)
|
|
||||||
|
|
||||||
for child_key, value in data.items():
|
|
||||||
node.children[child_key] = self.create_node(child_key, value, level + 1)
|
|
||||||
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _create_value_node(self, key: Any, data: Any) -> ValueNode:
|
|
||||||
hint = NODE_OBJECT if key == TAG_OBJECT else None
|
|
||||||
return ValueNode(data, hint)
|
|
||||||
|
|
||||||
def get_node_by_id(self, node_id: str) -> tuple[Any, Node]:
|
|
||||||
return self._nodes_by_id[node_id]
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clears all stored nodes"""
|
|
||||||
self._nodes_by_id.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class JsonViewerHelper:
|
|
||||||
class_string = f"mmt-jsonviewer-string"
|
|
||||||
class_bool = f"mmt-jsonviewer-bool"
|
|
||||||
class_number = f"mmt-jsonviewer-number"
|
|
||||||
class_null = f"mmt-jsonviewer-null"
|
|
||||||
class_digest = f"mmt-jsonviewer-digest"
|
|
||||||
class_object = f"mmt-jsonviewer-object"
|
|
||||||
class_dataframe = f"mmt-jsonviewer-dataframe"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_sha256(_value):
|
|
||||||
return (isinstance(_value, str) and
|
|
||||||
len(_value) == 64 and
|
|
||||||
all(c in '0123456789abcdefABCDEF' for c in _value))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_quotes(value: str) -> str:
|
|
||||||
if '"' in value and "'" in value:
|
|
||||||
return f'"{value.replace("\"", "\\\"")}"'
|
|
||||||
elif '"' in value:
|
|
||||||
return f"'{value}'"
|
|
||||||
else:
|
|
||||||
return f'"{value}"'
|
|
||||||
|
|
||||||
|
|
||||||
class NodeRenderer:
|
|
||||||
"""Single class handling all node rendering with helper methods"""
|
|
||||||
|
|
||||||
def __init__(self, session, jsonviewer_instance, folding_manager: FoldingManager,
|
|
||||||
commands: JsonViewerCommands, helper: JsonViewerHelper,
|
|
||||||
hooks: list[tuple[Callable, Callable]] = None):
|
|
||||||
self.session = session
|
|
||||||
self.jsonviewer = jsonviewer_instance # reference to main component
|
|
||||||
self.folding_manager = folding_manager
|
|
||||||
self.commands = commands
|
|
||||||
self.helper = helper
|
|
||||||
self.hooks = hooks or []
|
|
||||||
|
|
||||||
def render(self, key: Any, node: Node) -> Div:
|
|
||||||
"""Main rendering method for any node"""
|
|
||||||
must_expand = self.folding_manager.must_expand(node)
|
|
||||||
|
|
||||||
return Div(
|
|
||||||
self._create_folding_icon(node, must_expand),
|
|
||||||
Span(f'{key} : ') if key is not None else None,
|
|
||||||
self._render_value(key, node, must_expand),
|
|
||||||
style=f"margin-left: {INDENT_SIZE}px;",
|
|
||||||
id=getattr(node, "node_id", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_folding_icon(self, node: Node, must_expand: Optional[bool]) -> Optional[Span]:
|
|
||||||
"""Creates folding/unfolding icon"""
|
|
||||||
if must_expand is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Span(
|
|
||||||
icon_expanded if must_expand else icon_collapsed,
|
|
||||||
cls="icon-16-inline mmt-jsonviewer-folding",
|
|
||||||
style=f"margin-left: -{INDENT_SIZE}px;",
|
|
||||||
**self.commands.fold(
|
|
||||||
node.node_id,
|
|
||||||
FoldingMode.COLLAPSE if must_expand else FoldingMode.EXPAND
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_value(self, key: Any, node: Node, must_expand: Optional[bool]) -> Any:
|
|
||||||
"""Renders the value part of a node - extracted from original _render_value"""
|
|
||||||
if must_expand is False:
|
|
||||||
return self._render_collapsed_indicator(node)
|
|
||||||
|
|
||||||
# Check hooks first
|
|
||||||
for predicate, renderer in self.hooks:
|
|
||||||
if predicate(key, node, self.helper):
|
|
||||||
return renderer(key, node, self.helper)
|
|
||||||
|
|
||||||
# Route to appropriate helper method
|
|
||||||
if isinstance(node, DictNode):
|
|
||||||
return self._render_dict_node(key, node)
|
|
||||||
elif isinstance(node, ListNode):
|
|
||||||
return self._render_list_node(key, node)
|
|
||||||
else:
|
|
||||||
return self._render_value_node(key, node)
|
|
||||||
|
|
||||||
def _render_collapsed_indicator(self, node: Node) -> Span:
|
|
||||||
"""Renders collapsed indicator - extracted from original"""
|
|
||||||
indicator = "[...]" if isinstance(node, ListNode) else "{...}"
|
|
||||||
return Span(
|
|
||||||
indicator,
|
|
||||||
id=node.node_id,
|
|
||||||
**self.commands.fold(node.node_id, FoldingMode.EXPAND)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_dict_node(self, key: Any, node: DictNode) -> Span:
|
|
||||||
"""Renders dictionary node - extracted from original _render_dict"""
|
|
||||||
return Span(
|
|
||||||
"{",
|
|
||||||
*[self.render(child_key, value) for child_key, value in node.children.items()],
|
|
||||||
Div("}"),
|
|
||||||
id=node.node_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_list_node(self, key: Any, node: ListNode) -> Span:
|
|
||||||
"""Renders list node - extracted from original _render_list"""
|
|
||||||
if self._should_render_list_as_grid(key, node):
|
|
||||||
return self._render_list_as_grid(key, node)
|
|
||||||
else:
|
|
||||||
return self._render_list_as_array(key, node)
|
|
||||||
|
|
||||||
def _should_render_list_as_grid(self, key: Any, node: ListNode) -> bool:
|
|
||||||
"""Determines if list should be rendered as grid - extracted from original _all_the_same"""
|
|
||||||
if len(node.children) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
sample_node = node.children[0]
|
|
||||||
sample_value = sample_node.value
|
|
||||||
|
|
||||||
if sample_value is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
type_ = type(sample_value)
|
|
||||||
if type_ in (int, float, str, bool, list, dict, ValueNode):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check if a specific rendering is specified via hooks
|
|
||||||
for predicate, renderer in self.hooks:
|
|
||||||
if predicate(key, sample_node, self.helper):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return all(type(item.value) == type_ for item in node.children)
|
|
||||||
|
|
||||||
def _render_list_as_grid(self, key: Any, node: ListNode) -> Span:
|
|
||||||
"""Renders list as grid - extracted from original _render_as_grid"""
|
|
||||||
type_ = type(node.children[0].value)
|
|
||||||
icon = icon_class
|
|
||||||
str_value = type_.__name__.split(".")[-1]
|
|
||||||
|
|
||||||
data = [child.value.__dict__ for child in node.children]
|
|
||||||
df = DataFrame(data)
|
|
||||||
dg = DataGrid(self.session)
|
|
||||||
dg.init_from_dataframe(df)
|
|
||||||
|
|
||||||
return Span(
|
|
||||||
Span(
|
|
||||||
Span(icon, cls="icon-16-inline mr-1"),
|
|
||||||
Span(str_value),
|
|
||||||
cls="mmt-jsonviewer-object"
|
|
||||||
),
|
|
||||||
dg,
|
|
||||||
id=node.node_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_list_as_array(self, key: Any, node: ListNode) -> Span:
|
|
||||||
"""Renders list as array - extracted from original _render_as_list"""
|
|
||||||
return Span(
|
|
||||||
"[",
|
|
||||||
*[self.render(index, item) for index, item in enumerate(node.children)],
|
|
||||||
Div("]"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_value_node(self, key: Any, node: ValueNode) -> Span:
|
|
||||||
"""Renders value node - extracted and simplified from original _render_value"""
|
|
||||||
data_tooltip = None
|
|
||||||
htmx_params = {}
|
|
||||||
icon = None
|
|
||||||
|
|
||||||
if isinstance(node.value, bool): # order is important bool is an int in Python !
|
|
||||||
str_value = "true" if node.value else "false"
|
|
||||||
data_class = "bool"
|
|
||||||
elif isinstance(node.value, (int, float)):
|
|
||||||
str_value = str(node.value)
|
|
||||||
data_class = "number"
|
|
||||||
elif node.value is None:
|
|
||||||
str_value = "null"
|
|
||||||
data_class = "null"
|
|
||||||
elif self.helper.is_sha256(node.value):
|
|
||||||
str_value = str(node.value)
|
|
||||||
data_class = "digest"
|
|
||||||
htmx_params = self.commands.open_digest(self.jsonviewer.user_id, node.value)
|
|
||||||
elif node.hint == NODE_OBJECT:
|
|
||||||
icon = icon_class
|
|
||||||
str_value = node.value.split(".")[-1]
|
|
||||||
data_class = "object"
|
|
||||||
elif isinstance(node.value, DataFrame):
|
|
||||||
return self._render_dataframe_value(node.value)
|
|
||||||
else:
|
|
||||||
str_value, data_tooltip = self._format_string_value(node.value)
|
|
||||||
data_class = "string"
|
|
||||||
|
|
||||||
return self._create_value_span(str_value, data_class, icon, data_tooltip, htmx_params)
|
|
||||||
|
|
||||||
def _render_dataframe_value(self, dataframe: DataFrame) -> Any:
|
|
||||||
"""Renders DataFrame value"""
|
|
||||||
dg = DataGrid(self.session)
|
|
||||||
dg.init_from_dataframe(dataframe)
|
|
||||||
return dg
|
|
||||||
|
|
||||||
def _format_string_value(self, value: Any) -> tuple[str, Optional[str]]:
|
|
||||||
"""Formats string value with tooltip if too long"""
|
|
||||||
as_str = str(value)
|
|
||||||
if len(as_str) > MAX_TEXT_LENGTH:
|
|
||||||
return as_str[:MAX_TEXT_LENGTH] + "...", as_str
|
|
||||||
else:
|
|
||||||
return self.helper.add_quotes(as_str), None
|
|
||||||
|
|
||||||
def _create_value_span(self, str_value: str, data_class: str, icon: Any,
|
|
||||||
data_tooltip: Optional[str], htmx_params: dict) -> Span:
|
|
||||||
"""Creates the final Span element for a value"""
|
|
||||||
css_class = f"mmt-jsonviewer-{data_class}"
|
|
||||||
if data_tooltip:
|
|
||||||
css_class += " mmt-tooltip"
|
|
||||||
|
|
||||||
if icon:
|
|
||||||
return Span(
|
|
||||||
Span(icon, cls="icon-16-inline mr-1"),
|
|
||||||
Span(str_value, data_tooltip=data_tooltip, **htmx_params),
|
|
||||||
cls=css_class
|
|
||||||
)
|
|
||||||
|
|
||||||
return Span(str_value, cls=css_class, data_tooltip=data_tooltip, **htmx_params)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonViewer(BaseComponentMultipleInstance):
|
|
||||||
"""Main JsonViewer component with separated concerns"""
|
|
||||||
|
|
||||||
COMPONENT_INSTANCE_ID = "Jsonviewer"
|
|
||||||
|
|
||||||
def __init__(self, session, _id, data=None, hooks=None, key=None, boundaries=None):
|
|
||||||
super().__init__(session, _id)
|
|
||||||
self._key = key
|
|
||||||
self.data = data
|
|
||||||
self.hooks = hooks or []
|
|
||||||
self._boundaries = boundaries if boundaries else {"height": "600"}
|
|
||||||
self._commands = JsonViewerCommands(self)
|
|
||||||
|
|
||||||
# Initialize helper components
|
|
||||||
self._helper = JsonViewerHelper()
|
|
||||||
self._id_generator = NodeIdGenerator(_id)
|
|
||||||
self._folding_manager = FoldingManager()
|
|
||||||
self._node_factory = NodeFactory(self._id_generator, self._folding_manager)
|
|
||||||
|
|
||||||
# Initialize single renderer
|
|
||||||
self._node_renderer = NodeRenderer(
|
|
||||||
session, self, self._folding_manager,
|
|
||||||
self._commands, self._helper, self.hooks
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the initial node tree
|
|
||||||
self.node = self._node_factory.create_node(None, data)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_id(self) -> str:
|
|
||||||
"""Gets user_id from session or returns default"""
|
|
||||||
# Preserve original behavior - this should be implemented based on your session management
|
|
||||||
return getattr(self, '_user_id', getattr(self._session, 'user_id', 'default_user'))
|
|
||||||
|
|
||||||
def set_data(self, data):
|
|
||||||
"""Updates the data and recreates the node tree"""
|
|
||||||
self.data = data
|
|
||||||
self._id_generator.reset()
|
|
||||||
self._node_factory.clear()
|
|
||||||
self.node = self._node_factory.create_node(None, data)
|
|
||||||
|
|
||||||
def set_node_folding(self, node_id: str, folding: str):
|
|
||||||
"""Sets folding state for a specific node"""
|
|
||||||
self._folding_manager.set_node_folding(node_id, folding)
|
|
||||||
|
|
||||||
def render_node(self, node_id: str):
|
|
||||||
"""Renders a specific node by ID"""
|
|
||||||
key, node = self._node_factory.get_node_by_id(node_id)
|
|
||||||
return self._node_renderer.render(key, node)
|
|
||||||
|
|
||||||
def set_folding_mode(self, folding_mode: str):
|
|
||||||
"""Sets global folding mode"""
|
|
||||||
self._folding_manager.set_folding_mode(folding_mode)
|
|
||||||
|
|
||||||
def get_folding_mode(self) -> str:
|
|
||||||
"""Gets current folding mode"""
|
|
||||||
return self._folding_manager.get_folding_mode()
|
|
||||||
|
|
||||||
def open_digest(self, user_id: str, digest: str):
|
|
||||||
"""Opens digest - preserves original method"""
|
|
||||||
return self._owner.db_engine_headers(user_id, digest)
|
|
||||||
|
|
||||||
def __ft__(self):
|
|
||||||
"""FastHTML rendering method"""
|
|
||||||
if self.node is None:
|
|
||||||
return Div("No data to display", cls="mmt-jsonviewer", id=f"{self._id}")
|
|
||||||
|
|
||||||
return Div(
|
|
||||||
Div(
|
|
||||||
self._node_renderer.render(None, self.node),
|
|
||||||
id=f"{self._id}-root",
|
|
||||||
style="margin-left: 0px;"
|
|
||||||
),
|
|
||||||
cls="mmt-jsonviewer",
|
|
||||||
id=f"{self._id}",
|
|
||||||
**apply_boundaries(self._boundaries)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
"""Equality comparison"""
|
|
||||||
if type(other) is type(self):
|
|
||||||
return self._key is not None and self._key == other._key
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
"""Hash method"""
|
|
||||||
return hash(self._key) if self._key is not None else super().__hash__()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
ROUTE_ROOT = "/jsonviewer"
|
|
||||||
|
|
||||||
INDENT_SIZE = 20
|
|
||||||
MAX_TEXT_LENGTH = 50
|
|
||||||
|
|
||||||
NODE_OBJECT = "Object"
|
|
||||||
NODES_KEYS_TO_NOT_EXPAND = ["Dataframe", "__parent__"]
|
|
||||||
|
|
||||||
class Routes:
|
|
||||||
Fold = "/fold"
|
|
||||||
@@ -46,7 +46,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
self.properties = WorkflowDesignerProperties(self._session, f"{self._id}", self)
|
self.properties = WorkflowDesignerProperties(self._session, f"{self._id}", self)
|
||||||
|
|
||||||
workflow_name = self._designer_settings.workflow_name
|
workflow_name = self._designer_settings.workflow_name
|
||||||
self.player = InstanceManager.get(self._session,
|
self._player = InstanceManager.get(self._session,
|
||||||
WorkflowPlayer.create_component_id(self._session, workflow_name),
|
WorkflowPlayer.create_component_id(self._session, workflow_name),
|
||||||
WorkflowPlayer,
|
WorkflowPlayer,
|
||||||
settings_manager=self._settings_manager,
|
settings_manager=self._settings_manager,
|
||||||
@@ -222,23 +222,22 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
def play_workflow(self, boundaries: dict):
|
def play_workflow(self, boundaries: dict):
|
||||||
self._error_message = None
|
self._error_message = None
|
||||||
|
|
||||||
self.player.run()
|
self._player.run()
|
||||||
if self.player.global_error:
|
if self._player.global_error:
|
||||||
# Show the error message in the same tab
|
# Show the error message in the same tab
|
||||||
self._error_message = self.player.global_error
|
self._error_message = self._player.global_error
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.properties.set_entry_selector_data(self.player.nb_items)
|
|
||||||
# change the tab and display the results
|
# change the tab and display the results
|
||||||
self.player.set_boundaries(boundaries)
|
self._player.set_boundaries(boundaries)
|
||||||
self.tabs_manager.add_tab(f"Workflow {self._designer_settings.workflow_name}", self.player, self.player.key)
|
self.tabs_manager.add_tab(f"Workflow {self._designer_settings.workflow_name}", self._player, self._player.key)
|
||||||
|
|
||||||
return self.tabs_manager.refresh()
|
return self.tabs_manager.refresh()
|
||||||
|
|
||||||
def stop_workflow(self):
|
def stop_workflow(self):
|
||||||
self._error_message = None
|
self._error_message = None
|
||||||
self.player.stop()
|
self._player.stop()
|
||||||
self.properties.set_entry_selector_data(0)
|
|
||||||
return self.tabs_manager.refresh()
|
return self.tabs_manager.refresh()
|
||||||
|
|
||||||
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
||||||
@@ -315,7 +314,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
|
|
||||||
def _mk_component(self, component: WorkflowComponent):
|
def _mk_component(self, component: WorkflowComponent):
|
||||||
|
|
||||||
runtime_state = self.player.get_component_runtime_state(component.id)
|
runtime_state = self._player.get_component_runtime_state(component.id)
|
||||||
|
|
||||||
info = COMPONENT_TYPES[component.type]
|
info = COMPONENT_TYPES[component.type]
|
||||||
is_selected = self._state.selected_component_id == component.id
|
is_selected = self._state.selected_component_id == component.id
|
||||||
@@ -510,7 +509,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _mk_properties(self, oob=False):
|
def _mk_properties(self, oob=False):
|
||||||
return self.properties.__ft__(oob)
|
return self.properties
|
||||||
|
|
||||||
def _mk_jira_processor_details(self, component):
|
def _mk_jira_processor_details(self, component):
|
||||||
def _mk_option(name):
|
def _mk_option(name):
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.entryselector.components.EntrySelector import EntrySelector
|
|
||||||
from components.jsonviewer.components.JsonViewer import JsonViewer
|
|
||||||
from components.workflows.constants import COMPONENT_TYPES, PROCESSOR_TYPES
|
from components.workflows.constants import COMPONENT_TYPES, PROCESSOR_TYPES
|
||||||
from components_helpers import mk_dialog_buttons
|
from components_helpers import mk_dialog_buttons
|
||||||
from core.instance_manager import InstanceManager
|
|
||||||
from core.jira import JiraRequestTypes, DEFAULT_SEARCH_FIELDS
|
from core.jira import JiraRequestTypes, DEFAULT_SEARCH_FIELDS
|
||||||
from utils.DbManagementHelper import DbManagementHelper
|
from utils.DbManagementHelper import DbManagementHelper
|
||||||
|
|
||||||
@@ -27,18 +25,6 @@ class WorkflowDesignerProperties(BaseComponent):
|
|||||||
self._component = None
|
self._component = None
|
||||||
self.update_layout()
|
self.update_layout()
|
||||||
self.update_component(self._owner.get_state().selected_component_id)
|
self.update_component(self._owner.get_state().selected_component_id)
|
||||||
self.entry_selector: EntrySelector = InstanceManager.new(self._session,
|
|
||||||
EntrySelector,
|
|
||||||
owner=self,
|
|
||||||
hooks={
|
|
||||||
"on_entry_selected": self.on_entry_selector_changed})
|
|
||||||
self._input_jsonviewer: JsonViewer = InstanceManager.new(self._session,
|
|
||||||
JsonViewer)
|
|
||||||
self._output_jsonviewer: JsonViewer = InstanceManager.new(self._session,
|
|
||||||
JsonViewer)
|
|
||||||
|
|
||||||
def set_entry_selector_data(self, data):
|
|
||||||
self.entry_selector.set_data(data)
|
|
||||||
|
|
||||||
def update_layout(self):
|
def update_layout(self):
|
||||||
if self._owner.get_state().properties_input_width is None:
|
if self._owner.get_state().properties_input_width is None:
|
||||||
@@ -69,54 +55,29 @@ class WorkflowDesignerProperties(BaseComponent):
|
|||||||
|
|
||||||
return self.__ft__(oob=oob)
|
return self.__ft__(oob=oob)
|
||||||
|
|
||||||
def on_entry_selector_changed(self, entry):
|
|
||||||
entry = int(entry)
|
|
||||||
|
|
||||||
input_data, output_data = None, None
|
|
||||||
selected_component_id = self._owner.get_state().selected_component_id
|
|
||||||
if selected_component_id is not None:
|
|
||||||
runtime_state = self._owner.player.runtime_states.get(selected_component_id, None)
|
|
||||||
if runtime_state is not None:
|
|
||||||
input_content = runtime_state.input[entry] if len(runtime_state.input) > entry else None
|
|
||||||
output_content = runtime_state.output[entry] if len(runtime_state.output) > entry else None
|
|
||||||
if input_content is not None:
|
|
||||||
self._input_jsonviewer.set_data(input_content.item.as_dict())
|
|
||||||
input_data = self._input_jsonviewer
|
|
||||||
if output_content is not None:
|
|
||||||
self._output_jsonviewer.set_data(output_content.item.as_dict())
|
|
||||||
output_data = self._output_jsonviewer
|
|
||||||
|
|
||||||
return (self._mk_input(content=input_data, oob=True),
|
|
||||||
self._mk_output(content=output_data, oob=True))
|
|
||||||
|
|
||||||
def _mk_layout(self):
|
def _mk_layout(self):
|
||||||
return Div(
|
return Div(
|
||||||
self.entry_selector,
|
|
||||||
Div(
|
|
||||||
self._mk_input(),
|
self._mk_input(),
|
||||||
self._mk_properties(),
|
self._mk_properties(),
|
||||||
self._mk_output(),
|
self._mk_output(),
|
||||||
cls="flex",
|
cls="flex",
|
||||||
style="height: 100%; width: 100%; flex: 1;"
|
style="height: 100%; width: 100%; flex: 1;"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_input(self, content=None, oob=False):
|
def _mk_input(self):
|
||||||
return Div(
|
return Div(
|
||||||
content,
|
"Input",
|
||||||
id=f"pi_{self._id}",
|
id=f"pi_{self._id}",
|
||||||
style=f"width: {self.layout.input_width}px;",
|
style=f"width: {self.layout.input_width}px;",
|
||||||
cls="wkf-properties-input",
|
cls="wkf-properties-input"
|
||||||
hx_swap_oob=f'true' if oob else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_output(self, content=None, oob=False):
|
def _mk_output(self):
|
||||||
return Div(
|
return Div(
|
||||||
content,
|
"Output",
|
||||||
id=f"po_{self._id}",
|
id=f"po_{self._id}",
|
||||||
style=f"width: {self.layout.output_width}px;",
|
style=f"width: {self.layout.output_width}px;",
|
||||||
cls="wkf-properties-output",
|
cls="wkf-properties-output"
|
||||||
hx_swap_oob=f'true' if oob else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_properties(self):
|
def _mk_properties(self):
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class WorkflowPlayer(BaseComponent):
|
|||||||
self.runtime_states = {}
|
self.runtime_states = {}
|
||||||
self.global_error = None
|
self.global_error = None
|
||||||
self.has_error = False
|
self.has_error = False
|
||||||
self.nb_items = 0
|
|
||||||
|
|
||||||
def set_boundaries(self, boundaries: dict):
|
def set_boundaries(self, boundaries: dict):
|
||||||
self._datagrid.set_boundaries(boundaries)
|
self._datagrid.set_boundaries(boundaries)
|
||||||
@@ -94,14 +93,11 @@ class WorkflowPlayer(BaseComponent):
|
|||||||
self.global_error = engine.global_error
|
self.global_error = engine.global_error
|
||||||
|
|
||||||
else: # loop through the components and update the runtime states
|
else: # loop through the components and update the runtime states
|
||||||
self.nb_items = engine.nb_items
|
|
||||||
for component in sorted_components:
|
for component in sorted_components:
|
||||||
runtime_state = self.runtime_states.get(component.id)
|
runtime_state = self.runtime_states.get(component.id)
|
||||||
|
|
||||||
if component.id not in engine.errors:
|
if component.id not in engine.errors:
|
||||||
runtime_state.state = ComponentState.SUCCESS
|
runtime_state.state = ComponentState.SUCCESS
|
||||||
runtime_state.input = engine.debug[component.id]["input"]
|
|
||||||
runtime_state.output = engine.debug[component.id]["output"]
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# the component failed
|
# the component failed
|
||||||
@@ -181,7 +177,7 @@ class WorkflowPlayer(BaseComponent):
|
|||||||
# Return sorted components
|
# Return sorted components
|
||||||
return [components_by_id[cid] for cid in sorted_order]
|
return [components_by_id[cid] for cid in sorted_order]
|
||||||
|
|
||||||
def _get_engine(self, sorted_components) -> WorkflowEngine:
|
def _get_engine(self, sorted_components):
|
||||||
# first reorder the component, according to the connection definitions
|
# first reorder the component, according to the connection definitions
|
||||||
engine = WorkflowEngine()
|
engine = WorkflowEngine()
|
||||||
for component in sorted_components:
|
for component in sorted_components:
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ class WorkflowComponentRuntimeState:
|
|||||||
id: str
|
id: str
|
||||||
state: ComponentState = ComponentState.SUCCESS
|
state: ComponentState = ComponentState.SUCCESS
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
input: list = None
|
|
||||||
output: list = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -64,7 +62,7 @@ class WorkflowsDesignerState:
|
|||||||
component_counter: int = 0
|
component_counter: int = 0
|
||||||
designer_height: int = 230
|
designer_height: int = 230
|
||||||
properties_input_width: int = None
|
properties_input_width: int = None
|
||||||
properties_properties_width: int = None
|
properties_properties_width : int = None
|
||||||
properties_output_width: int = None
|
properties_output_width: int = None
|
||||||
selected_component_id: str | None = None
|
selected_component_id: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ class Expando:
|
|||||||
return self._props.copy()
|
return self._props.copy()
|
||||||
|
|
||||||
def to_dict(self, mappings: dict) -> dict:
|
def to_dict(self, mappings: dict) -> dict:
|
||||||
"""
|
|
||||||
Return the information as a dictionary, with the given mappings
|
|
||||||
"""
|
|
||||||
return {prop_name: self.get(path) for path, prop_name in mappings.items() if prop_name is not None}
|
return {prop_name: self.get(path) for path, prop_name in mappings.items() if prop_name is not None}
|
||||||
|
|
||||||
def __hasattr__(self, item):
|
def __hasattr__(self, item):
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ class InstanceManager:
|
|||||||
|
|
||||||
return InstanceManager._instances[key]
|
return InstanceManager._instances[key]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new(session, instance_type, **kwargs):
|
|
||||||
return InstanceManager.get(session, instance_type.create_component_id(session), instance_type, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register(session: dict | None, instance, instance_id: str = None):
|
def register(session: dict | None, instance, instance_id: str = None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
34
src/main.py
34
src/main.py
@@ -1,5 +1,7 @@
|
|||||||
# global layout
|
# global layout
|
||||||
import logging.config
|
import logging.config
|
||||||
|
import random
|
||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
@@ -53,6 +55,9 @@ links = [
|
|||||||
Link(href="./assets/daisyui-5-themes.css", rel="stylesheet", type="text/css"),
|
Link(href="./assets/daisyui-5-themes.css", rel="stylesheet", type="text/css"),
|
||||||
Script(src="./assets/tailwindcss-browser@4.js"),
|
Script(src="./assets/tailwindcss-browser@4.js"),
|
||||||
|
|
||||||
|
# SSE
|
||||||
|
Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),
|
||||||
|
|
||||||
# Old drawer layout
|
# Old drawer layout
|
||||||
Script(src="./assets/DrawerLayout.js", defer=True),
|
Script(src="./assets/DrawerLayout.js", defer=True),
|
||||||
Link(rel="stylesheet", href="./assets/DrawerLayout.css"),
|
Link(rel="stylesheet", href="./assets/DrawerLayout.css"),
|
||||||
@@ -146,8 +151,6 @@ register_component("theme_controller", "components.themecontroller", "ThemeContr
|
|||||||
register_component("main_layout", "components.drawerlayout", "DrawerLayoutApp")
|
register_component("main_layout", "components.drawerlayout", "DrawerLayoutApp")
|
||||||
register_component("undo_redo", "components.undo_redo", "UndoRedoApp")
|
register_component("undo_redo", "components.undo_redo", "UndoRedoApp")
|
||||||
register_component("tabs", "components.tabs", "TabsApp") # before repositories
|
register_component("tabs", "components.tabs", "TabsApp") # before repositories
|
||||||
register_component("entryselector", "components.entryselector", "EntrySelectorApp")
|
|
||||||
register_component("jsonviewer", "components.jsonviewer", "JsonViewerApp")
|
|
||||||
register_component("applications", "components.applications", "ApplicationsApp")
|
register_component("applications", "components.applications", "ApplicationsApp")
|
||||||
register_component("repositories", "components.repositories", "RepositoriesApp")
|
register_component("repositories", "components.repositories", "RepositoriesApp")
|
||||||
register_component("workflows", "components.workflows", "WorkflowsApp")
|
register_component("workflows", "components.workflows", "WorkflowsApp")
|
||||||
@@ -216,7 +219,7 @@ app, rt = fast_app(
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
# Profiling middleware
|
# Profiling middleware
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# @app.middleware("http")
|
@app.middleware("http")
|
||||||
async def timing_middleware(request, call_next):
|
async def timing_middleware(request, call_next):
|
||||||
import time
|
import time
|
||||||
start_total = time.perf_counter()
|
start_total = time.perf_counter()
|
||||||
@@ -273,6 +276,31 @@ def get(session):
|
|||||||
DrawerLayoutOld(pages),)
|
DrawerLayoutOld(pages),)
|
||||||
|
|
||||||
|
|
||||||
|
shutdown_event = signal_shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
async def number_generator():
|
||||||
|
while True: # not shutdown_event.is_set():
|
||||||
|
data = Article(random.randint(1, 100))
|
||||||
|
print(data)
|
||||||
|
yield sse_message(data)
|
||||||
|
await sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@rt("/sse")
|
||||||
|
def get():
|
||||||
|
return Titled("SSE Random Number Generator",
|
||||||
|
P("Generate pairs of random numbers, as the list grows scroll downwards."),
|
||||||
|
Div(hx_ext="sse",
|
||||||
|
sse_connect="/number-stream",
|
||||||
|
hx_swap="beforeend show:bottom",
|
||||||
|
sse_swap="message"))
|
||||||
|
|
||||||
|
|
||||||
|
@rt("/number-stream")
|
||||||
|
async def get(): return EventStream(number_generator())
|
||||||
|
|
||||||
|
|
||||||
@rt('/toasting')
|
@rt('/toasting')
|
||||||
def get(session):
|
def get(session):
|
||||||
# Normally one toast is enough, this allows us to see
|
# Normally one toast is enough, this allows us to see
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import ast
|
import ast
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Generator
|
from typing import Any, Generator
|
||||||
|
|
||||||
from components.admin.admin_db_manager import AdminDbManager
|
from components.admin.admin_db_manager import AdminDbManager
|
||||||
@@ -12,14 +11,6 @@ from core.utils import UnreferencedNamesVisitor
|
|||||||
from utils.Datahelper import DataHelper
|
from utils.Datahelper import DataHelper
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WorkflowPayload:
|
|
||||||
processor_name: str
|
|
||||||
component_id: str
|
|
||||||
item_linkage_id: int
|
|
||||||
item: Any
|
|
||||||
|
|
||||||
|
|
||||||
class DataProcessorError(Exception):
|
class DataProcessorError(Exception):
|
||||||
def __init__(self, component_id, error):
|
def __init__(self, component_id, error):
|
||||||
self.component_id = component_id
|
self.component_id = component_id
|
||||||
@@ -155,56 +146,35 @@ class WorkflowEngine:
|
|||||||
self.has_error = False
|
self.has_error = False
|
||||||
self.global_error = None
|
self.global_error = None
|
||||||
self.errors = {}
|
self.errors = {}
|
||||||
self.debug = {}
|
|
||||||
self.nb_items = -1
|
|
||||||
|
|
||||||
def add_processor(self, processor: DataProcessor) -> 'WorkflowEngine':
|
def add_processor(self, processor: DataProcessor) -> 'WorkflowEngine':
|
||||||
"""Add a data processor to the pipeline."""
|
"""Add a data processor to the pipeline."""
|
||||||
self.processors.append(processor)
|
self.processors.append(processor)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _process_single_item(self, item_linkage_id, item: Any, processor_index: int = 0) -> Generator[Any, None, None]:
|
def _process_single_item(self, item: Any, processor_index: int = 0) -> Generator[Any, None, None]:
|
||||||
"""Process a single item through the remaining processors."""
|
"""Process a single item through the remaining processors."""
|
||||||
if processor_index >= len(self.processors):
|
if processor_index >= len(self.processors):
|
||||||
yield item
|
yield item
|
||||||
return
|
return
|
||||||
|
|
||||||
processor = self.processors[processor_index]
|
processor = self.processors[processor_index]
|
||||||
if not processor.component_id in self.debug:
|
|
||||||
self.debug[processor.component_id] = {"input": [], "output": []}
|
|
||||||
|
|
||||||
self.debug[processor.component_id]["input"].append(WorkflowPayload(
|
|
||||||
processor_name=processor.__class__.__name__,
|
|
||||||
component_id=processor.component_id,
|
|
||||||
item_linkage_id=item_linkage_id,
|
|
||||||
item=item))
|
|
||||||
|
|
||||||
# Process the item through the current processor
|
# Process the item through the current processor
|
||||||
for processed_item in processor.process(item):
|
for processed_item in processor.process(item):
|
||||||
self.debug[processor.component_id]["output"].append(WorkflowPayload(
|
|
||||||
processor_name=processor.__class__.__name__,
|
|
||||||
component_id=processor.component_id,
|
|
||||||
item_linkage_id=item_linkage_id,
|
|
||||||
item=processed_item))
|
|
||||||
|
|
||||||
# Recursively process through remaining processors
|
# Recursively process through remaining processors
|
||||||
yield from self._process_single_item(item_linkage_id, processed_item, processor_index + 1)
|
yield from self._process_single_item(processed_item, processor_index + 1)
|
||||||
|
|
||||||
def run(self) -> Generator[Any, None, None]:
|
def run(self) -> Generator[Any, None, None]:
|
||||||
"""
|
"""
|
||||||
Run the workflow pipeline and yield results one by one.
|
Run the workflow pipeline and yield results one by one.
|
||||||
The first processor must be a DataProducer.
|
The first processor must be a DataProducer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.debug.clear()
|
|
||||||
|
|
||||||
if not self.processors:
|
if not self.processors:
|
||||||
self.has_error = False
|
self.has_error = False
|
||||||
self.global_error = "No processors in the pipeline"
|
self.global_error = "No processors in the pipeline"
|
||||||
self.nb_items = -1
|
|
||||||
raise ValueError(self.global_error)
|
raise ValueError(self.global_error)
|
||||||
|
|
||||||
self.nb_items = 0
|
|
||||||
first_processor = self.processors[0]
|
first_processor = self.processors[0]
|
||||||
|
|
||||||
if not isinstance(first_processor, DataProducer):
|
if not isinstance(first_processor, DataProducer):
|
||||||
@@ -212,16 +182,8 @@ class WorkflowEngine:
|
|||||||
self.global_error = "First processor must be a DataProducer"
|
self.global_error = "First processor must be a DataProducer"
|
||||||
raise ValueError(self.global_error)
|
raise ValueError(self.global_error)
|
||||||
|
|
||||||
self.debug[first_processor.component_id] = {"input": [], "output": []}
|
for item in first_processor.process(None):
|
||||||
|
yield from self._process_single_item(item, 1)
|
||||||
for item_linkage_id, item in enumerate(first_processor.process(None)):
|
|
||||||
self.nb_items += 1
|
|
||||||
self.debug[first_processor.component_id]["output"].append(WorkflowPayload(
|
|
||||||
processor_name=first_processor.__class__.__name__,
|
|
||||||
component_id=first_processor.component_id,
|
|
||||||
item_linkage_id=item_linkage_id,
|
|
||||||
item=item))
|
|
||||||
yield from self._process_single_item(item_linkage_id, item, 1)
|
|
||||||
|
|
||||||
def run_to_list(self) -> list[Any]:
|
def run_to_list(self) -> list[Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from components.jsonviewer.components.JsonViewer import *
|
from components.debugger.components.JsonViewer import *
|
||||||
from helpers import matches, span_icon, search_elements_by_name, extract_jsonviewer_node
|
from helpers import matches, span_icon, search_elements_by_name, extract_jsonviewer_node
|
||||||
|
|
||||||
JSON_VIEWER_INSTANCE_ID = "json_viewer"
|
JSON_VIEWER_INSTANCE_ID = "json_viewer"
|
||||||
ML_20 = "margin-left: 20px;"
|
ML_20 = "margin-left: 20px;"
|
||||||
CLS_PREFIX = "mmt-jsonviewer"
|
CLS_PREFIX = "mmt-jsonviewer"
|
||||||
|
USER_ID = "user_id"
|
||||||
|
|
||||||
dn = DictNode
|
dn = DictNode
|
||||||
ln = ListNode
|
ln = ListNode
|
||||||
@@ -14,7 +15,7 @@ n = ValueNode
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def json_viewer(session):
|
def json_viewer(session):
|
||||||
return JsonViewer(session, JSON_VIEWER_INSTANCE_ID, {})
|
return JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -40,7 +41,7 @@ def jv_id(x):
|
|||||||
ln([{"a": [1, 2]}], jv_id(0), 0, [dn({"a": [1, 2]}, jv_id(1), 1, {"a": ln([1, 2], jv_id(2), 2, [n(1), n(2)])})]))
|
ln([{"a": [1, 2]}], jv_id(0), 0, [dn({"a": [1, 2]}, jv_id(1), 1, {"a": ln([1, 2], jv_id(2), 2, [n(1), n(2)])})]))
|
||||||
])
|
])
|
||||||
def test_i_can_create_node(data, expected_node):
|
def test_i_can_create_node(data, expected_node):
|
||||||
json_viewer_ = JsonViewer(None, JSON_VIEWER_INSTANCE_ID, data)
|
json_viewer_ = JsonViewer(None, JSON_VIEWER_INSTANCE_ID, None, USER_ID, data)
|
||||||
assert json_viewer_.node == expected_node
|
assert json_viewer_.node == expected_node
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ def test_i_can_render(json_viewer):
|
|||||||
(None, Span("null", cls=f"{CLS_PREFIX}-null")),
|
(None, Span("null", cls=f"{CLS_PREFIX}-null")),
|
||||||
])
|
])
|
||||||
def test_i_can_render_simple_value(session, value, expected_inner):
|
def test_i_can_render_simple_value(session, value, expected_inner):
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
actual = jsonv.__ft__()
|
actual = jsonv.__ft__()
|
||||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id('root')}"})[0]
|
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id('root')}"})[0]
|
||||||
expected = Div(
|
expected = Div(
|
||||||
@@ -80,7 +81,7 @@ def test_i_can_render_simple_value(session, value, expected_inner):
|
|||||||
|
|
||||||
def test_i_can_render_expanded_list_node(session):
|
def test_i_can_render_expanded_list_node(session):
|
||||||
value = [1, "hello", True]
|
value = [1, "hello", True]
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
# Force expansion of the node
|
# Force expansion of the node
|
||||||
jsonv.set_folding_mode("expand")
|
jsonv.set_folding_mode("expand")
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ def test_i_can_render_expanded_list_node(session):
|
|||||||
|
|
||||||
def test_i_can_render_expanded_dict_node(session):
|
def test_i_can_render_expanded_dict_node(session):
|
||||||
value = {"a": 1, "b": "hello", "c": True}
|
value = {"a": 1, "b": "hello", "c": True}
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
# Force expansion of the node
|
# Force expansion of the node
|
||||||
jsonv.set_folding_mode("expand")
|
jsonv.set_folding_mode("expand")
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ def test_i_can_render_expanded_dict_node(session):
|
|||||||
|
|
||||||
def test_i_can_render_expanded_list_of_dict_node(session):
|
def test_i_can_render_expanded_list_of_dict_node(session):
|
||||||
value = [{"a": 1, "b": "hello"}]
|
value = [{"a": 1, "b": "hello"}]
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
# Force expansion of all nodes
|
# Force expansion of all nodes
|
||||||
jsonv.set_folding_mode("expand")
|
jsonv.set_folding_mode("expand")
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ def test_i_can_render_expanded_list_of_dict_node(session):
|
|||||||
def test_render_with_collapse_folding_mode(session):
|
def test_render_with_collapse_folding_mode(session):
|
||||||
# Create a nested structure to test collapse rendering
|
# Create a nested structure to test collapse rendering
|
||||||
value = {"a": [1, 2, 3], "b": {"x": "y", "z": True}}
|
value = {"a": [1, 2, 3], "b": {"x": "y", "z": True}}
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
|
|
||||||
# Ensure folding mode is set to collapse (should be default)
|
# Ensure folding mode is set to collapse (should be default)
|
||||||
jsonv.set_folding_mode("collapse")
|
jsonv.set_folding_mode("collapse")
|
||||||
@@ -194,7 +195,7 @@ def test_render_with_collapse_folding_mode(session):
|
|||||||
def test_render_with_specific_node_expanded_in_collapse_mode(session):
|
def test_render_with_specific_node_expanded_in_collapse_mode(session):
|
||||||
# Create a nested structure to test mixed collapse/expand rendering
|
# Create a nested structure to test mixed collapse/expand rendering
|
||||||
value = {"a": [1, 2, 3], "b": {"x": "y", "z": True}}
|
value = {"a": [1, 2, 3], "b": {"x": "y", "z": True}}
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
|
|
||||||
# Ensure folding mode is set to collapse
|
# Ensure folding mode is set to collapse
|
||||||
jsonv.set_folding_mode(FoldingMode.COLLAPSE)
|
jsonv.set_folding_mode(FoldingMode.COLLAPSE)
|
||||||
@@ -229,7 +230,7 @@ def test_render_with_specific_node_expanded_in_collapse_mode(session):
|
|||||||
def test_multiple_folding_levels_in_collapse_mode(session):
|
def test_multiple_folding_levels_in_collapse_mode(session):
|
||||||
# Create a deeply nested structure
|
# Create a deeply nested structure
|
||||||
value = {"level1": {"level2": {"level3": [1, 2, 3]}}}
|
value = {"level1": {"level2": {"level3": [1, 2, 3]}}}
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
|
|
||||||
# Set folding mode to collapse
|
# Set folding mode to collapse
|
||||||
jsonv.set_folding_mode(FoldingMode.COLLAPSE)
|
jsonv.set_folding_mode(FoldingMode.COLLAPSE)
|
||||||
@@ -261,7 +262,7 @@ def test_multiple_folding_levels_in_collapse_mode(session):
|
|||||||
|
|
||||||
def test_toggle_between_folding_modes(session):
|
def test_toggle_between_folding_modes(session):
|
||||||
value = {"a": [1, 2, 3], "b": {"x": "y"}}
|
value = {"a": [1, 2, 3], "b": {"x": "y"}}
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, value)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||||
|
|
||||||
# Start with collapse mode
|
# Start with collapse mode
|
||||||
jsonv.set_folding_mode("collapse")
|
jsonv.set_folding_mode("collapse")
|
||||||
@@ -270,19 +271,19 @@ def test_toggle_between_folding_modes(session):
|
|||||||
jsonv.set_node_folding(f"{JSON_VIEWER_INSTANCE_ID}-0", "expand")
|
jsonv.set_node_folding(f"{JSON_VIEWER_INSTANCE_ID}-0", "expand")
|
||||||
|
|
||||||
# Verify node is in tracked nodes (exceptions to collapse mode)
|
# Verify node is in tracked nodes (exceptions to collapse mode)
|
||||||
assert f"{JSON_VIEWER_INSTANCE_ID}-0" in jsonv._folding_manager.get_nodes_to_track()
|
assert f"{JSON_VIEWER_INSTANCE_ID}-0" in jsonv._nodes_to_track
|
||||||
|
|
||||||
# Now switch to expand mode
|
# Now switch to expand mode
|
||||||
jsonv.set_folding_mode("expand")
|
jsonv.set_folding_mode("expand")
|
||||||
|
|
||||||
# Tracked nodes should be cleared
|
# Tracked nodes should be cleared
|
||||||
assert len(jsonv._folding_manager.get_nodes_to_track()) == 0
|
assert len(jsonv._nodes_to_track) == 0
|
||||||
|
|
||||||
# Collapse specific node
|
# Collapse specific node
|
||||||
jsonv.set_node_folding(f"{JSON_VIEWER_INSTANCE_ID}-0", "collapse")
|
jsonv.set_node_folding(f"{JSON_VIEWER_INSTANCE_ID}-0", "collapse")
|
||||||
|
|
||||||
# Verify node is in tracked nodes (exceptions to expand mode)
|
# Verify node is in tracked nodes (exceptions to expand mode)
|
||||||
assert f"{JSON_VIEWER_INSTANCE_ID}-0" in jsonv._folding_manager.get_nodes_to_track()
|
assert f"{JSON_VIEWER_INSTANCE_ID}-0" in jsonv._nodes_to_track
|
||||||
|
|
||||||
# Render and verify the output
|
# Render and verify the output
|
||||||
actual = jsonv.__ft__()
|
actual = jsonv.__ft__()
|
||||||
@@ -306,7 +307,7 @@ def test_custom_hook_rendering(session, helper):
|
|||||||
hooks = [(custom_predicate, custom_renderer)]
|
hooks = [(custom_predicate, custom_renderer)]
|
||||||
|
|
||||||
# Create JsonViewer with the custom hook
|
# Create JsonViewer with the custom hook
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, "custom_hook_test", hooks=hooks)
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, "custom_hook_test", hooks=hooks)
|
||||||
|
|
||||||
actual = jsonv.__ft__()
|
actual = jsonv.__ft__()
|
||||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id('root')}"})[0]
|
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id('root')}"})[0]
|
||||||
@@ -323,7 +324,7 @@ def test_custom_hook_rendering(session, helper):
|
|||||||
|
|
||||||
|
|
||||||
def test_folding_mode_operations(session):
|
def test_folding_mode_operations(session):
|
||||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, {"a": [1, 2, 3]})
|
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, {"a": [1, 2, 3]})
|
||||||
|
|
||||||
# Check default folding mode
|
# Check default folding mode
|
||||||
assert jsonv.get_folding_mode() == "collapse"
|
assert jsonv.get_folding_mode() == "collapse"
|
||||||
@@ -337,11 +338,11 @@ def test_folding_mode_operations(session):
|
|||||||
jsonv.set_node_folding(node_id, "collapse")
|
jsonv.set_node_folding(node_id, "collapse")
|
||||||
|
|
||||||
# Node should be in tracked nodes since it differs from the default mode
|
# Node should be in tracked nodes since it differs from the default mode
|
||||||
assert node_id in jsonv._folding_manager.get_nodes_to_track()
|
assert node_id in jsonv._nodes_to_track
|
||||||
|
|
||||||
# Restore to match default mode
|
# Restore to match default mode
|
||||||
jsonv.set_node_folding(node_id, "expand")
|
jsonv.set_node_folding(node_id, "expand")
|
||||||
assert node_id not in jsonv._folding_manager.get_nodes_to_track()
|
assert node_id not in jsonv._nodes_to_track
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input_value, expected_output", [
|
@pytest.mark.parametrize("input_value, expected_output", [
|
||||||
@@ -352,7 +353,7 @@ def test_folding_mode_operations(session):
|
|||||||
('', '""'), # Empty string
|
('', '""'), # Empty string
|
||||||
])
|
])
|
||||||
def test_add_quotes(input_value, expected_output):
|
def test_add_quotes(input_value, expected_output):
|
||||||
result = JsonViewerHelper.add_quotes(input_value)
|
result = JsonViewer.add_quotes(input_value)
|
||||||
assert result == expected_output
|
assert result == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ def test_run_simple_workflow(engine):
|
|||||||
assert result == [1, 2, 3]
|
assert result == [1, 2, 3]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Not yet implemented")
|
|
||||||
def test_process_single_item(engine):
|
def test_process_single_item(engine):
|
||||||
"""Test the internal _process_single_item method."""
|
"""Test the internal _process_single_item method."""
|
||||||
mock_processor = MagicMock(spec=DataProcessor)
|
mock_processor = MagicMock(spec=DataProcessor)
|
||||||
|
|||||||
Reference in New Issue
Block a user