Refactoring DataGrid to use DataService.py
This commit is contained in:
58
src/app.py
58
src/app.py
@@ -1,9 +1,7 @@
|
||||
import json
|
||||
import logging.config
|
||||
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from dbengine.handlers import BaseRefHandler, handlers
|
||||
from dbengine.handlers import handlers
|
||||
from fasthtml import serve
|
||||
from fasthtml.components import Div
|
||||
|
||||
@@ -15,7 +13,6 @@ from myfasthtml.controls.InstancesDebugger import InstancesDebugger
|
||||
from myfasthtml.controls.Keyboard import Keyboard
|
||||
from myfasthtml.controls.Layout import Layout
|
||||
from myfasthtml.controls.TabsManager import TabsManager
|
||||
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
from myfasthtml.core.dbengine_utils import DataFrameHandler
|
||||
from myfasthtml.core.instances import UniqueInstance
|
||||
@@ -39,53 +36,6 @@ app, rt = create_app(protect_routes=True,
|
||||
base_url="http://localhost:5003")
|
||||
|
||||
|
||||
|
||||
def create_sample_treeview(parent):
|
||||
"""
|
||||
Create a sample TreeView with a file structure for testing.
|
||||
|
||||
Args:
|
||||
parent: Parent instance for the TreeView
|
||||
|
||||
Returns:
|
||||
TreeView: Configured TreeView instance with sample data
|
||||
"""
|
||||
tree_view = TreeView(parent, _id="-treeview")
|
||||
|
||||
# Create sample file structure
|
||||
projects = TreeNode(label="Projects", type="folder")
|
||||
tree_view.add_node(projects)
|
||||
|
||||
myfasthtml = TreeNode(label="MyFastHtml", type="folder")
|
||||
tree_view.add_node(myfasthtml, parent_id=projects.id)
|
||||
|
||||
app_py = TreeNode(label="app.py", type="file")
|
||||
tree_view.add_node(app_py, parent_id=myfasthtml.id)
|
||||
|
||||
readme = TreeNode(label="README.md", type="file")
|
||||
tree_view.add_node(readme, parent_id=myfasthtml.id)
|
||||
|
||||
src_folder = TreeNode(label="src", type="folder")
|
||||
tree_view.add_node(src_folder, parent_id=myfasthtml.id)
|
||||
|
||||
controls_py = TreeNode(label="controls.py", type="file")
|
||||
tree_view.add_node(controls_py, parent_id=src_folder.id)
|
||||
|
||||
documents = TreeNode(label="Documents", type="folder")
|
||||
tree_view.add_node(documents, parent_id=projects.id)
|
||||
|
||||
notes = TreeNode(label="notes.txt", type="file")
|
||||
tree_view.add_node(notes, parent_id=documents.id)
|
||||
|
||||
todo = TreeNode(label="todo.md", type="file")
|
||||
tree_view.add_node(todo, parent_id=documents.id)
|
||||
|
||||
# Expand all nodes to show the full structure
|
||||
# tree_view.expand_all()
|
||||
|
||||
return tree_view
|
||||
|
||||
|
||||
@rt("/")
|
||||
def index(session):
|
||||
session_instance = UniqueInstance(session=session,
|
||||
@@ -120,19 +70,15 @@ def index(session):
|
||||
btn_popup = mk.label("Popup",
|
||||
command=add_tab("Popup", Dropdown(layout, "Content", button="button", _id="-dropdown")))
|
||||
|
||||
# Create TreeView with sample data
|
||||
tree_view = create_sample_treeview(layout)
|
||||
|
||||
layout.header_left.add(tabs_manager.add_tab_btn())
|
||||
layout.header_right.add(btn_show_right_drawer)
|
||||
layout.left_drawer.add(btn_show_instances_debugger, "Debugger")
|
||||
layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
|
||||
layout.left_drawer.add(btn_file_upload, "Test")
|
||||
layout.left_drawer.add(btn_popup, "Test")
|
||||
layout.left_drawer.add(tree_view, "TreeView")
|
||||
|
||||
# data grids
|
||||
dgs_manager = DataGridsManager(layout, _id="-datagrids")
|
||||
dgs_manager = DataGridsManager(session_instance)
|
||||
layout.left_drawer.add_group("Documents", Div("Documents",
|
||||
dgs_manager.mk_main_icons(),
|
||||
cls="mf-layout-group flex gap-3"))
|
||||
|
||||
@@ -5,10 +5,8 @@ from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from fasthtml.common import NotStr
|
||||
from fasthtml.components import *
|
||||
from pandas import DataFrame
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.CycleStateControl import CycleStateControl
|
||||
@@ -17,23 +15,28 @@ from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEdito
|
||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Keyboard import Keyboard
|
||||
from myfasthtml.controls.Mouse import Mouse
|
||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||
from myfasthtml.controls.Query import Query, QUERY_FILTER
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||
from myfasthtml.controls.helpers import mk, column_type_defaults
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowUiState, \
|
||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState, DataGridColumnUiState, \
|
||||
DataGridRowSelectionColumnState
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID, \
|
||||
ROW_SELECTION_ID
|
||||
from myfasthtml.core.constants import ColumnType, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
|
||||
from myfasthtml.core.data.DataService import DataService
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.dsls import DslsManager
|
||||
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine
|
||||
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||
from myfasthtml.core.formatting.dsl.parser import DSLParser
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.core.instances import MultipleInstance, InstancesManager
|
||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
||||
from myfasthtml.core.utils import make_safe_id, merge_classes, make_unique_safe_id, is_null
|
||||
from myfasthtml.core.utils import merge_classes, is_null
|
||||
from myfasthtml.icons.carbon import row, column, grid
|
||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular, add12_filled
|
||||
@@ -70,8 +73,8 @@ class DatagridState(DbObject):
|
||||
super().__init__(owner, name=f"{owner.get_id()}#state", save_state=save_state)
|
||||
self.sidebar_visible: bool = False
|
||||
self.selected_view: str = None
|
||||
self.columns: list[DataGridColumnState] = []
|
||||
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
|
||||
self.columns: list[DataGridColumnUiState] = []
|
||||
self.rows: list[DataGridRowUiState] = [] # only the rows that have a specific state
|
||||
self.headers: list[DataGridHeaderFooterConf] = []
|
||||
self.footers: list[DataGridHeaderFooterConf] = []
|
||||
self.sorted: list = []
|
||||
@@ -101,20 +104,6 @@ class DatagridSettings(DbObject):
|
||||
self.enable_edition: bool = True
|
||||
|
||||
|
||||
class DatagridStore(DbObject):
|
||||
"""
|
||||
Store Dataframes
|
||||
"""
|
||||
|
||||
def __init__(self, owner, save_state):
|
||||
with self.initializing():
|
||||
super().__init__(owner, name=f"{owner.get_id()}#df", save_state=save_state)
|
||||
self.ne_df = None
|
||||
self.ns_fast_access = None
|
||||
self.ns_row_data = None
|
||||
self.ns_total_rows = None
|
||||
|
||||
|
||||
class Commands(BaseCommands):
|
||||
def get_page(self, page_index: int):
|
||||
return Command("GetPage",
|
||||
@@ -231,11 +220,15 @@ class DataGrid(MultipleInstance):
|
||||
name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__")
|
||||
self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
|
||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
||||
self._df_store = DatagridStore(self, save_state=self._settings.save_state)
|
||||
self._formatting_engine = FormattingEngine()
|
||||
self._columns = None
|
||||
self.commands = Commands(self)
|
||||
self.init_from_dataframe(self._df_store.ne_df, init_state=False) # data comes from DatagridStore
|
||||
|
||||
# Obtain DataService from DataServicesManager (no parent hierarchy)
|
||||
data_services_manager = InstancesManager.get_by_type(self._session, DataServicesManager)
|
||||
data_service_id = self.get_data_service_id_from_data_grid_id(self._id)
|
||||
self._data_service = data_services_manager.restore_service(data_service_id)
|
||||
self._init_columns()
|
||||
|
||||
# add Panel
|
||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
||||
@@ -253,15 +246,17 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
# add Selection Selector
|
||||
selection_types = {
|
||||
"cell": mk.icon(grid, tooltip="Cell selection"), # default
|
||||
"row": mk.icon(row, tooltip="Row selection"),
|
||||
"column": mk.icon(column, tooltip="Column selection"),
|
||||
"cell": mk.icon(grid, tooltip="Cell selection")
|
||||
}
|
||||
self._selection_mode_selector = CycleStateControl(self,
|
||||
controls=selection_types,
|
||||
save_state=False,
|
||||
_id="-cycle_state")
|
||||
self._selection_mode_selector.bind_command("CycleState", self.commands.change_selection_mode())
|
||||
if self._state.selection.selection_mode is None:
|
||||
self.change_selection_mode()
|
||||
|
||||
# add columns manager
|
||||
self._columns_manager = DataGridColumnsManager(self)
|
||||
@@ -269,7 +264,8 @@ class DataGrid(MultipleInstance):
|
||||
self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
|
||||
|
||||
if self._settings.enable_formatting:
|
||||
completion_engine = FormattingCompletionEngine(self._parent, self.get_table_name())
|
||||
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None)
|
||||
completion_engine = FormattingCompletionEngine(provider, self.get_table_name())
|
||||
editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
|
||||
dsl = FormattingDSL()
|
||||
self._formatting_editor = DataGridFormattingEditor(self,
|
||||
@@ -301,7 +297,13 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
@property
|
||||
def _df(self):
|
||||
return self._df_store.ne_df
|
||||
if self._data_service is None:
|
||||
return None
|
||||
return self._data_service.get_store().ne_df
|
||||
|
||||
@property
|
||||
def _fast_access(self):
|
||||
return self._data_service.get_store().ns_fast_access
|
||||
|
||||
def _apply_sort(self, df):
|
||||
if df is None:
|
||||
@@ -344,7 +346,8 @@ class DataGrid(MultipleInstance):
|
||||
df = self._df.copy()
|
||||
df = self._apply_sort(df) # need to keep the real type to sort
|
||||
df = self._apply_filter(df)
|
||||
self._df_store.ns_total_rows = len(df)
|
||||
if self._data_service is not None:
|
||||
self._data_service.get_store().ns_total_rows = len(df)
|
||||
|
||||
return df
|
||||
|
||||
@@ -375,36 +378,6 @@ class DataGrid(MultipleInstance):
|
||||
self._state.selection.selected = pos
|
||||
self._state.save()
|
||||
|
||||
def _register_existing_formulas(self) -> None:
|
||||
"""
|
||||
Re-register all formula columns with the FormulaEngine.
|
||||
|
||||
Called after data reload to ensure the engine knows about all
|
||||
formula columns and their expressions.
|
||||
"""
|
||||
engine = self.get_formula_engine()
|
||||
if engine is None:
|
||||
return
|
||||
table = self.get_table_name()
|
||||
for col_def in self._state.columns:
|
||||
if col_def.formula:
|
||||
try:
|
||||
engine.set_formula(table, col_def.col_id, col_def.formula)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to register formula for %s.%s: %s", table, col_def.col_id, e)
|
||||
|
||||
def _recalculate_formulas(self) -> None:
|
||||
"""
|
||||
Recalculate dirty formula columns before rendering.
|
||||
|
||||
Called at the start of mk_body_content_page() to ensure formula
|
||||
columns are up-to-date before cells are rendered.
|
||||
"""
|
||||
engine = self.get_formula_engine()
|
||||
if engine is None:
|
||||
return
|
||||
engine.recalculate_if_needed(self.get_table_name(), self._df_store)
|
||||
|
||||
def _get_format_rules(self, col_pos, row_index, col_def):
|
||||
"""
|
||||
Get format rules for a cell, returning only the most specific level defined.
|
||||
@@ -440,197 +413,55 @@ class DataGrid(MultipleInstance):
|
||||
if self._state.table_format:
|
||||
return self._state.table_format
|
||||
|
||||
# Get global tables formatting from manager
|
||||
return self._parent.all_tables_formats
|
||||
# Get global tables formatting from DatagridMetadataProvider
|
||||
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None)
|
||||
return provider.all_tables_formats if provider is not None else []
|
||||
|
||||
def _init_columns(self):
|
||||
self._columns = self._state.columns.copy()
|
||||
# Populate UI state from DataService columns when creating a new grid
|
||||
columns_defs = self._data_service.columns
|
||||
|
||||
columns = []
|
||||
if self._state.columns:
|
||||
# we need to make sure that we match the DataGridColumnUiState and the ColumnDefinition
|
||||
for col_ui_state in self._state.columns:
|
||||
col_def = [col_def for col_def in columns_defs if col_def.col_id == col_ui_state.col_id][0]
|
||||
columns.append(DataGridColumnState(col_def, col_ui_state))
|
||||
|
||||
self._columns = columns
|
||||
|
||||
else:
|
||||
# init the ui states
|
||||
ui_states = [DataGridColumnUiState(col_def.col_id) for col_def in columns_defs]
|
||||
self._state.columns = ui_states # this saves the state of the columns
|
||||
|
||||
self._columns = [DataGridColumnState(col_def, col_ui_state)
|
||||
for col_def, col_ui_state in zip(columns_defs, ui_states)]
|
||||
|
||||
if self._settings.enable_edition:
|
||||
self._columns.insert(0, DataGridColumnState(ROW_SELECTION_ID,
|
||||
-1,
|
||||
"",
|
||||
ColumnType.RowSelection_))
|
||||
|
||||
def init_from_dataframe(self, df, init_state=True):
|
||||
|
||||
def _get_column_type(dtype):
|
||||
if pd.api.types.is_integer_dtype(dtype):
|
||||
return ColumnType.Number
|
||||
elif pd.api.types.is_float_dtype(dtype):
|
||||
return ColumnType.Number
|
||||
elif pd.api.types.is_bool_dtype(dtype):
|
||||
return ColumnType.Bool
|
||||
elif pd.api.types.is_datetime64_any_dtype(dtype):
|
||||
return ColumnType.Datetime
|
||||
else:
|
||||
return ColumnType.Text # Default to Text if no match
|
||||
|
||||
def _init_columns(_df):
|
||||
columns = [DataGridColumnState(make_safe_id(col_id),
|
||||
col_index,
|
||||
col_id,
|
||||
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
||||
for col_index, col_id in enumerate(_df.columns)]
|
||||
|
||||
return columns
|
||||
|
||||
def _init_fast_access(_df):
|
||||
"""
|
||||
Generates a fast-access dictionary for a DataFrame.
|
||||
|
||||
This method converts the columns of the provided DataFrame into NumPy arrays
|
||||
and stores them as values in a dictionary, using the column names as keys.
|
||||
This allows for efficient access to the data stored in the DataFrame.
|
||||
|
||||
Args:
|
||||
_df (DataFrame): The input pandas DataFrame whose columns are to be converted
|
||||
into a dictionary of NumPy arrays.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where the keys are the column names of the input DataFrame
|
||||
and the values are the corresponding column values as NumPy arrays.
|
||||
"""
|
||||
if _df is None:
|
||||
return {}
|
||||
|
||||
res = {col: _df[col].to_numpy() for col in _df.columns}
|
||||
res[ROW_INDEX_ID] = _df.index.to_numpy()
|
||||
return res
|
||||
|
||||
def _init_row_data(_df):
|
||||
"""
|
||||
Generates a list of row data dictionaries for column references in formatting conditions.
|
||||
|
||||
Each dict contains {col_id: value} for a single row, used by FormattingEngine
|
||||
to evaluate conditions that reference other columns (e.g., {"col": "budget"}).
|
||||
|
||||
Args:
|
||||
_df (DataFrame): The input pandas DataFrame.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list where each element is a dict of column values for that row.
|
||||
"""
|
||||
if _df is None:
|
||||
return []
|
||||
|
||||
return _df.to_dict(orient='records')
|
||||
|
||||
if df is not None:
|
||||
self._df_store.ne_df = df
|
||||
if init_state:
|
||||
self._df.columns = self._df.columns.map(make_safe_id) # make sure column names are trimmed
|
||||
self._df_store.save()
|
||||
self._state.rows = [] # sparse: only rows with non-default state are stored
|
||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
||||
|
||||
self._init_columns()
|
||||
self._df_store.ns_fast_access = _init_fast_access(self._df)
|
||||
self._df_store.ns_row_data = _init_row_data(self._df)
|
||||
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||
self._register_existing_formulas()
|
||||
|
||||
return self
|
||||
self._columns.insert(0, DataGridRowSelectionColumnState())
|
||||
|
||||
def add_new_column(self, col_def: DataGridColumnState) -> None:
|
||||
"""Add a new column to the DataGrid.
|
||||
|
||||
For Formula columns, only _state.columns is updated; the FormulaEngine
|
||||
computes values on demand via recalculate_if_needed().
|
||||
|
||||
For other column types, also adds the column to the DataFrame and updates
|
||||
ns_fast_access and ns_row_data incrementally with type-appropriate defaults.
|
||||
"""Add a new column, delegating data mutation to DataService.
|
||||
|
||||
Args:
|
||||
col_def: Column definition with title and type already set.
|
||||
col_id is derived from title via make_safe_id.
|
||||
col_def: Column definition with title and type set. col_id will be
|
||||
assigned by DataService.
|
||||
"""
|
||||
col_def.col_id = make_unique_safe_id(col_def.title, [c.col_id for c in self._state.columns])
|
||||
|
||||
if col_def.type == ColumnType.Formula:
|
||||
col_def.col_index = -1
|
||||
self._state.columns.append(col_def)
|
||||
return
|
||||
|
||||
default_value = column_type_defaults.get(col_def.type, "")
|
||||
col_def.col_index = len(self._df.columns) if self._df is not None else 0
|
||||
self._state.columns.append(col_def)
|
||||
|
||||
if self._df is not None:
|
||||
self._df_store.ne_df[col_def.col_id] = default_value
|
||||
self._df_store.ns_fast_access[col_def.col_id] = self._df_store.ne_df[col_def.col_id].to_numpy()
|
||||
for row_dict in self._df_store.ns_row_data:
|
||||
row_dict[col_def.col_id] = default_value
|
||||
self._df_store.save()
|
||||
|
||||
def add_new_row(self, row_data: dict = None) -> None:
|
||||
"""Add a new row to the DataGrid with incremental updates.
|
||||
|
||||
Creates default values based on column types and handles formula columns.
|
||||
Updates ns_fast_access and ns_row_data incrementally for better performance.
|
||||
|
||||
Args:
|
||||
row_data: Optional dict with initial values. If None, uses type defaults.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
def _get_default_value(col_def: DataGridColumnState):
|
||||
"""Get default value for a column based on its type."""
|
||||
if col_def.type == ColumnType.Formula:
|
||||
return None # Will be calculated by FormulaEngine
|
||||
return column_type_defaults.get(col_def.type, "")
|
||||
|
||||
if row_data is None:
|
||||
# Create default values for all non-formula, non-selection columns
|
||||
row_data = {}
|
||||
for col in self._state.columns:
|
||||
if col.type not in (ColumnType.Formula, ColumnType.RowSelection_):
|
||||
row_data[col.col_id] = _get_default_value(col)
|
||||
|
||||
# 1. Add row to DataFrame (only non-formula columns)
|
||||
new_index = len(self._df)
|
||||
self._df.loc[new_index] = row_data
|
||||
|
||||
# 2. Incremental update of ns_fast_access
|
||||
for col_id, value in row_data.items():
|
||||
if col_id in self._df_store.ns_fast_access:
|
||||
self._df_store.ns_fast_access[col_id] = np.append(
|
||||
self._df_store.ns_fast_access[col_id],
|
||||
value
|
||||
)
|
||||
else:
|
||||
# First value for this column (rare case)
|
||||
self._df_store.ns_fast_access[col_id] = np.array([value])
|
||||
|
||||
# Update row index
|
||||
if ROW_INDEX_ID in self._df_store.ns_fast_access:
|
||||
self._df_store.ns_fast_access[ROW_INDEX_ID] = np.append(
|
||||
self._df_store.ns_fast_access[ROW_INDEX_ID],
|
||||
new_index
|
||||
if self._data_service is not None:
|
||||
data_col = ColumnDefinition(
|
||||
col_id="",
|
||||
col_index=col_def.col_index,
|
||||
title=col_def.title,
|
||||
type=col_def.type,
|
||||
formula=col_def.formula,
|
||||
)
|
||||
else:
|
||||
self._df_store.ns_fast_access[ROW_INDEX_ID] = np.array([new_index])
|
||||
|
||||
# 3. Incremental update of ns_row_data
|
||||
self._df_store.ns_row_data.append(row_data.copy())
|
||||
|
||||
# 4. Handle formula columns
|
||||
engine = self.get_formula_engine()
|
||||
if engine is not None:
|
||||
table_name = self.get_table_name()
|
||||
# Check if there are any formula columns
|
||||
has_formulas = any(col.type == ColumnType.Formula for col in self._state.columns)
|
||||
if has_formulas:
|
||||
try:
|
||||
# Recalculate all formulas (engine handles efficiency)
|
||||
engine.recalculate_if_needed(table_name, self._df_store)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to calculate formulas for new row: {e}")
|
||||
|
||||
# 5. Update total row count
|
||||
self._df_store.ns_total_rows = len(self._df)
|
||||
|
||||
# Save changes
|
||||
self._df_store.save()
|
||||
self._data_service.add_column(data_col)
|
||||
col_def.col_id = data_col.col_id
|
||||
col_def.col_index = data_col.col_index
|
||||
self._state.columns.append(col_def)
|
||||
self._init_columns()
|
||||
self._state.save()
|
||||
|
||||
def move_column(self, source_col_id: str, target_col_id: str):
|
||||
"""Move column to new position. Called via Command from JS."""
|
||||
@@ -676,7 +507,7 @@ class DataGrid(MultipleInstance):
|
||||
Returns:
|
||||
Optimal width in pixels (between 50 and 500)
|
||||
"""
|
||||
col_def = next((c for c in self._state.columns if c.col_id == col_id), None)
|
||||
col_def = next((c for c in self._columns if c.col_id == col_id), None)
|
||||
if not col_def:
|
||||
logger.warning(f"calculate_optimal_column_width: column not found {col_id=}")
|
||||
return 150 # default width
|
||||
@@ -686,8 +517,8 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
# Max data length
|
||||
max_data_length = 0
|
||||
if col_id in self._df_store.ns_fast_access:
|
||||
col_array = self._df_store.ns_fast_access[col_id]
|
||||
if col_id in self._fast_access:
|
||||
col_array = self._fast_access[col_id]
|
||||
if col_array is not None and len(col_array) > 0:
|
||||
max_data_length = max(len(str(v)) for v in col_array)
|
||||
|
||||
@@ -774,7 +605,6 @@ class DataGrid(MultipleInstance):
|
||||
def change_selection_mode(self):
|
||||
logger.debug(f"change_selection_mode")
|
||||
new_state = self._selection_mode_selector.get_state()
|
||||
logger.debug(f" {new_state=}")
|
||||
self._state.selection.selection_mode = new_state
|
||||
self._state.save()
|
||||
return self.render_partial()
|
||||
@@ -812,7 +642,8 @@ class DataGrid(MultipleInstance):
|
||||
self._state.save()
|
||||
|
||||
def handle_add_row(self):
|
||||
self.add_new_row() # Add row to data
|
||||
if self._data_service is not None:
|
||||
self._data_service.add_row()
|
||||
row_index = len(self._df) - 1 # Index of the newly added row
|
||||
len_columns_1 = len(self._columns) - 1
|
||||
rows = [self.mk_row(row_index, None, len_columns_1)]
|
||||
@@ -835,8 +666,17 @@ class DataGrid(MultipleInstance):
|
||||
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
|
||||
|
||||
def get_formula_engine(self):
|
||||
"""Return the FormulaEngine from the DataGridsManager, if available."""
|
||||
return self._parent.get_formula_engine()
|
||||
"""Return the shared FormulaEngine from DataServicesManager."""
|
||||
manager = InstancesManager.get_by_type(self._session, DataServicesManager, None)
|
||||
return manager.get_formula_engine() if manager is not None else None
|
||||
|
||||
@staticmethod
|
||||
def get_grid_id_from_data_service_id(data_service_id):
|
||||
return data_service_id.replace(DataService.compute_prefix(), DataGrid.compute_prefix(), 1)
|
||||
|
||||
@staticmethod
|
||||
def get_data_service_id_from_data_grid_id(datagrid_id):
|
||||
return datagrid_id.replace(DataGrid.compute_prefix(), DataService.compute_prefix(), 1)
|
||||
|
||||
def mk_headers(self):
|
||||
resize_cmd = self.commands.set_column_width()
|
||||
@@ -917,7 +757,7 @@ class DataGrid(MultipleInstance):
|
||||
# Formula column: safe read — ns_fast_access entry may not exist yet if the
|
||||
# engine hasn't run its first recalculation pass.
|
||||
if column_type == ColumnType.Formula:
|
||||
col_array = self._df_store.ns_fast_access.get(col_def.col_id)
|
||||
col_array = self._fast_access.get(col_def.col_id)
|
||||
if col_array is None or row_index >= len(col_array):
|
||||
return NotStr('<span class="dt2-cell-content-text truncate">—</span>')
|
||||
value = col_array[row_index]
|
||||
@@ -932,7 +772,7 @@ class DataGrid(MultipleInstance):
|
||||
else:
|
||||
column_type = ColumnType.Text
|
||||
else:
|
||||
value = self._df_store.ns_fast_access[col_def.col_id][row_index]
|
||||
value = self._fast_access[col_def.col_id][row_index]
|
||||
|
||||
# Boolean type - uses cached HTML (only 2 possible values)
|
||||
if column_type == ColumnType.Bool:
|
||||
@@ -981,7 +821,7 @@ class DataGrid(MultipleInstance):
|
||||
if col_def.type == ColumnType.RowSelection_:
|
||||
return OptimizedDiv(cls="dt2-row-selection")
|
||||
|
||||
col_array = self._df_store.ns_fast_access.get(col_def.col_id)
|
||||
col_array = self._fast_access.get(col_def.col_id)
|
||||
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
|
||||
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
||||
|
||||
@@ -1013,7 +853,8 @@ class DataGrid(MultipleInstance):
|
||||
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
|
||||
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
||||
"""
|
||||
self._recalculate_formulas()
|
||||
if self._data_service is not None:
|
||||
self._data_service.ensure_ready()
|
||||
df = self._get_filtered_df()
|
||||
if df is None:
|
||||
return []
|
||||
@@ -1026,7 +867,8 @@ class DataGrid(MultipleInstance):
|
||||
len_columns_1 = len(self._columns) - 1
|
||||
|
||||
rows = [self.mk_row(row_index, filter_keyword_lower, len_columns_1) for row_index in df.index[start:end]]
|
||||
if self._df_store.ns_total_rows > end:
|
||||
store = self._data_service.get_store() if self._data_service is not None else None
|
||||
if store is not None and store.ns_total_rows > end:
|
||||
rows[-1].attrs.extend(self.commands.get_page(page_index + 1).get_htmx_params(escaped=True))
|
||||
|
||||
self._mk_append_add_row_if_needed(rows)
|
||||
@@ -1184,7 +1026,7 @@ class DataGrid(MultipleInstance):
|
||||
)
|
||||
|
||||
def render(self):
|
||||
if self._df_store.ne_df is None:
|
||||
if self._df is None:
|
||||
return Div("No data to display !")
|
||||
|
||||
return Div(
|
||||
@@ -1201,8 +1043,10 @@ class DataGrid(MultipleInstance):
|
||||
cls="flex items-center justify-between mb-2"),
|
||||
self._panel.set_main(self.mk_table_wrapper()),
|
||||
Script(f"initDataGrid('{self._id}');"),
|
||||
# Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
||||
Keyboard(self, combinations=self._key_support, _id="-keyboard"),
|
||||
Div(
|
||||
Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
||||
Keyboard(self, combinations=self._key_support, _id="-keyboard"),
|
||||
),
|
||||
id=self._id,
|
||||
cls="grid",
|
||||
style="height: 100%; grid-template-rows: auto 1fr;"
|
||||
|
||||
@@ -159,24 +159,20 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
return self.mk_column_details(col_def)
|
||||
|
||||
def _register_formula(self, col_def) -> None:
|
||||
"""Register or remove a formula column with the FormulaEngine.
|
||||
"""Register or remove a formula column with the FormulaEngine via DataService.
|
||||
|
||||
Registers only when col_def.type is Formula and the formula text is
|
||||
non-empty. Removes the formula in all other cases so the engine stays
|
||||
consistent with the column definition.
|
||||
"""
|
||||
engine = self._parent.get_formula_engine()
|
||||
if engine is None:
|
||||
data_service = getattr(self._parent, "_data_service", None)
|
||||
if data_service is None:
|
||||
return
|
||||
table = self._parent.get_table_name()
|
||||
if col_def.type == ColumnType.Formula and col_def.formula:
|
||||
try:
|
||||
engine.set_formula(table, col_def.col_id, col_def.formula)
|
||||
logger.debug("Registered formula for %s.%s", table, col_def.col_id)
|
||||
except Exception as e:
|
||||
logger.warning("Formula error for %s.%s: %s", table, col_def.col_id, e)
|
||||
data_service.register_formula(col_def.col_id, col_def.formula)
|
||||
logger.debug("Registered formula for col %s", col_def.col_id)
|
||||
else:
|
||||
engine.remove_formula(table, col_def.col_id)
|
||||
data_service.remove_formula(col_def.col_id)
|
||||
|
||||
def mk_column_label(self, col_def: DataGridColumnState):
|
||||
return Div(
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from myfasthtml.controls.DslEditor import DslEditor
|
||||
from myfasthtml.controls.datagrid_objects import DataGridRowState
|
||||
from myfasthtml.controls.datagrid_objects import DataGridRowUiState
|
||||
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope, TableScope, TablesScope
|
||||
from myfasthtml.core.instances import InstancesManager
|
||||
|
||||
@@ -111,7 +111,7 @@ class DataGridFormattingEditor(DslEditor):
|
||||
for row_index, rules in rows_rules.items():
|
||||
row_state = next((r for r in state.rows if r.row_id == row_index), None)
|
||||
if row_state is None:
|
||||
row_state = DataGridRowState(row_id=row_index)
|
||||
row_state = DataGridRowUiState(row_id=row_index)
|
||||
state.rows.append(row_state)
|
||||
row_state.format = rules
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ from myfasthtml.controls.TreeView import TreeView, TreeNode, TreeViewConf
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
|
||||
from myfasthtml.core.formula.engine import FormulaEngine
|
||||
from myfasthtml.core.instances import InstancesManager, SingleInstance
|
||||
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
||||
from myfasthtml.icons.fluent_p3 import folder_open20_regular
|
||||
@@ -50,7 +48,7 @@ class Commands(BaseCommands):
|
||||
return Command("NewGrid",
|
||||
"New grid",
|
||||
self._owner,
|
||||
self._owner.new_grid).htmx(target=f"#{self._owner._tree.get_id()}")
|
||||
self._owner.handle_new_grid).htmx(target=f"#{self._owner._tree.get_id()}")
|
||||
|
||||
def open_from_excel(self, tab_id, file_upload):
|
||||
return Command("OpenFromExcel",
|
||||
@@ -81,11 +79,16 @@ class Commands(BaseCommands):
|
||||
key="DeleteNode")
|
||||
|
||||
|
||||
class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
class DataGridsManager(SingleInstance):
|
||||
"""UI manager for DataGrids.
|
||||
|
||||
Responsible for the visual organisation of DataGrids: TreeView, TabsManager,
|
||||
and document lifecycle (create, open, delete). All data concerns are handled
|
||||
by DataServicesManager and DataService.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, _id=None):
|
||||
if not getattr(self, "_is_new_instance", False):
|
||||
# Skip __init__ if instance already existed
|
||||
return
|
||||
super().__init__(parent, _id=_id)
|
||||
self.commands = Commands(self)
|
||||
@@ -96,53 +99,47 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager, None)
|
||||
self._registry = DataGridsRegistry(parent)
|
||||
|
||||
# Global presets shared across all DataGrids
|
||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||
self.all_tables_formats: list = []
|
||||
|
||||
# Formula engine shared across all DataGrids in this session
|
||||
self._formula_engine = FormulaEngine(
|
||||
registry_resolver=self._resolve_store_for_table
|
||||
)
|
||||
# Data layer — session-scoped singletons
|
||||
self._data_services_manager = DataServicesManager(self._parent)
|
||||
|
||||
def upload_from_source(self):
|
||||
file_upload = FileUpload(self)
|
||||
tab_id = self._tabs_manager.create_tab("Upload Datagrid", file_upload)
|
||||
file_upload.set_on_ok(self.commands.open_from_excel(tab_id, file_upload))
|
||||
return self._tabs_manager.show_tab(tab_id)
|
||||
# ------------------------------------------------------------------
|
||||
# Grid lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _create_and_register_grid(self, namespace: str, name: str, df: pd.DataFrame) -> DataGrid:
|
||||
"""
|
||||
Create and register a DataGrid.
|
||||
"""Create a DataGrid and its companion DataService, then register both.
|
||||
|
||||
Args:
|
||||
namespace: Grid namespace
|
||||
name: Grid name
|
||||
df: DataFrame to initialize the grid with
|
||||
namespace: Grid namespace.
|
||||
name: Grid name.
|
||||
df: DataFrame to initialise the grid with.
|
||||
|
||||
Returns:
|
||||
Created DataGrid instance
|
||||
Created DataGrid instance.
|
||||
"""
|
||||
table_name = f"{namespace}.{name}" if namespace else name
|
||||
data_service = self._data_services_manager.create_service(table_name, save_state=True)
|
||||
data_service.load_dataframe(df)
|
||||
|
||||
grid_id = DataGrid.get_grid_id_from_data_service_id(data_service.get_id())
|
||||
dg_conf = DatagridConf(namespace=namespace, name=name)
|
||||
dg = DataGrid(self, conf=dg_conf, save_state=True)
|
||||
dg.init_from_dataframe(df)
|
||||
dg = DataGrid(self, conf=dg_conf, save_state=True, _id=grid_id)
|
||||
|
||||
self._registry.put(namespace, name, dg.get_id())
|
||||
return dg
|
||||
|
||||
def _create_document(self, namespace: str, name: str, datagrid: DataGrid, tab_id: str = None) -> tuple[
|
||||
str, DocumentDefinition]:
|
||||
"""
|
||||
Create a DocumentDefinition and its associated tab.
|
||||
"""Create a DocumentDefinition and its associated tab.
|
||||
|
||||
Args:
|
||||
namespace: Document namespace
|
||||
name: Document name
|
||||
datagrid: Associated DataGrid instance
|
||||
tab_id: Optional existing tab ID. If None, creates a new tab
|
||||
namespace: Document namespace.
|
||||
name: Document name.
|
||||
datagrid: Associated DataGrid instance.
|
||||
tab_id: Optional existing tab ID. If None, creates a new tab.
|
||||
|
||||
Returns:
|
||||
Tuple of (tab_id, document)
|
||||
Tuple of (tab_id, document).
|
||||
"""
|
||||
if tab_id is None:
|
||||
tab_id = self._tabs_manager.create_tab(name, datagrid)
|
||||
@@ -159,15 +156,14 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
return tab_id, document
|
||||
|
||||
def _add_document_to_tree(self, document: DocumentDefinition, parent_id: str) -> TreeNode:
|
||||
"""
|
||||
Add a document to the tree view.
|
||||
"""Add a document node to the tree view.
|
||||
|
||||
Args:
|
||||
document: Document to add
|
||||
parent_id: Parent node ID in the tree
|
||||
document: Document to add.
|
||||
parent_id: Parent node ID in the tree.
|
||||
|
||||
Returns:
|
||||
Created TreeNode
|
||||
Created TreeNode.
|
||||
"""
|
||||
tree_node = TreeNode(
|
||||
id=document.document_id,
|
||||
@@ -179,8 +175,17 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
self._tree.add_node(tree_node, parent_id=parent_id)
|
||||
return tree_node
|
||||
|
||||
def new_grid(self):
|
||||
# Determine parent folder
|
||||
# ------------------------------------------------------------------
|
||||
# Commands handlers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def upload_from_source(self):
|
||||
file_upload = FileUpload(self)
|
||||
tab_id = self._tabs_manager.create_tab("Upload Datagrid", file_upload)
|
||||
file_upload.set_on_ok(self.commands.open_from_excel(tab_id, file_upload))
|
||||
return self._tabs_manager.show_tab(tab_id)
|
||||
|
||||
def handle_new_grid(self):
|
||||
selected_id = self._tree.get_selected_id()
|
||||
if selected_id is None:
|
||||
parent_id = self._tree.ensure_path("Untitled")
|
||||
@@ -188,52 +193,40 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
node = self._tree._state.items[selected_id]
|
||||
if node.type == "folder":
|
||||
parent_id = selected_id
|
||||
else: # leaf
|
||||
else:
|
||||
parent_id = node.parent
|
||||
|
||||
# Get namespace and generate unique name
|
||||
namespace = self._tree._state.items[parent_id].label
|
||||
namespace = self._tree.get_state().items[parent_id].label
|
||||
name = self._generate_unique_sheet_name(parent_id)
|
||||
|
||||
# Create and register DataGrid
|
||||
dg = self._create_and_register_grid(namespace, name, pd.DataFrame())
|
||||
|
||||
# Create document and tab
|
||||
tab_id, document = self._create_document(namespace, name, dg)
|
||||
|
||||
# Add to tree
|
||||
self._add_document_to_tree(document, parent_id)
|
||||
|
||||
# UI-specific handling: open parent, select node, start rename
|
||||
if parent_id not in self._tree._state.opened:
|
||||
self._tree._state.opened.append(parent_id)
|
||||
self._tree._state.selected = document.document_id
|
||||
if parent_id not in self._tree.get_state().opened:
|
||||
self._tree.get_state().opened.append(parent_id)
|
||||
self._tree.get_state().selected = document.document_id
|
||||
self._tree._start_rename(document.document_id)
|
||||
|
||||
return self._tree, self._tabs_manager.show_tab(tab_id)
|
||||
|
||||
def _generate_unique_sheet_name(self, parent_id: str) -> str:
|
||||
children = self._tree._state.items[parent_id].children
|
||||
existing_labels = {self._tree._state.items[c].label for c in children}
|
||||
children = self._tree.get_state().items[parent_id].children
|
||||
existing_labels = {self._tree.get_state().items[c].label for c in children}
|
||||
n = 1
|
||||
while f"Sheet{n}" in existing_labels:
|
||||
n += 1
|
||||
return f"Sheet{n}"
|
||||
|
||||
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
||||
# Read Excel data
|
||||
excel_content = file_upload.get_content()
|
||||
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
|
||||
namespace = file_upload.get_file_basename()
|
||||
name = file_upload.get_sheet_name()
|
||||
|
||||
# Create and register DataGrid
|
||||
dg = self._create_and_register_grid(namespace, name, df)
|
||||
|
||||
# Create document with existing tab
|
||||
tab_id, document = self._create_document(namespace, name, dg, tab_id=tab_id)
|
||||
|
||||
# Add to tree
|
||||
parent_id = self._tree.ensure_path(document.namespace)
|
||||
self._add_document_to_tree(document, parent_id)
|
||||
|
||||
@@ -243,80 +236,61 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
document_id = self._tree.get_bag(node_id)
|
||||
try:
|
||||
document = next(filter(lambda x: x.document_id == document_id, self._state.elements))
|
||||
dg = DataGrid(self, _id=document.datagrid_id) # reload the state & settings
|
||||
dg = DataGrid(self, _id=document.datagrid_id)
|
||||
return self._tabs_manager.show_or_create_tab(document.tab_id, document.name, dg)
|
||||
except StopIteration:
|
||||
# the selected node is not a document (it's a folder)
|
||||
return None
|
||||
|
||||
def delete_grid(self, node_id):
|
||||
"""
|
||||
Delete a grid and all its associated resources.
|
||||
"""Delete a grid and all its associated resources.
|
||||
|
||||
This method is called BEFORE TreeView._delete_node() to ensure we can
|
||||
access the node's bag to retrieve the document_id.
|
||||
Called BEFORE TreeView._delete_node() so we can access the node bag.
|
||||
|
||||
Args:
|
||||
node_id: ID of the TreeView node to delete
|
||||
node_id: ID of the TreeView node to delete.
|
||||
|
||||
Returns:
|
||||
None (TreeView will handle the node removal)
|
||||
List of UI updates, or None.
|
||||
"""
|
||||
document_id = self._tree.get_bag(node_id)
|
||||
if document_id is None:
|
||||
# Node is a folder, not a document - nothing to clean up
|
||||
return None
|
||||
|
||||
res = []
|
||||
try:
|
||||
# Find the document
|
||||
document = next(filter(lambda x: x.document_id == document_id, self._state.elements))
|
||||
|
||||
# Get the DataGrid instance
|
||||
dg = DataGrid(self, _id=document.datagrid_id)
|
||||
|
||||
# Close the tab
|
||||
close_tab_res = self._tabs_manager.close_tab(document.tab_id)
|
||||
res.append(close_tab_res)
|
||||
|
||||
# Remove from registry
|
||||
self._registry.remove(document.datagrid_id)
|
||||
self._data_services_manager.remove_service(document.datagrid_id)
|
||||
|
||||
# Clean up DataGrid (delete DBEngine entries)
|
||||
dg.delete()
|
||||
|
||||
# Remove from InstancesManager
|
||||
InstancesManager.remove(self._session, document.datagrid_id)
|
||||
|
||||
# Remove DocumentDefinition from state
|
||||
self._state.elements = [d for d in self._state.elements if d.document_id != document_id]
|
||||
self._state.save()
|
||||
|
||||
except StopIteration:
|
||||
# Document not found - already deleted or invalid state
|
||||
pass
|
||||
|
||||
return res
|
||||
|
||||
def create_tab_content(self, tab_id):
|
||||
"""
|
||||
Recreate the content for a tab managed by this DataGridsManager.
|
||||
Called by TabsManager when the content is not in cache (e.g., after restart).
|
||||
"""Recreate tab content after restart.
|
||||
|
||||
Args:
|
||||
tab_id: ID of the tab to recreate content for
|
||||
tab_id: ID of the tab to recreate.
|
||||
|
||||
Returns:
|
||||
The recreated component (Panel with DataGrid)
|
||||
DataGrid instance for the tab.
|
||||
"""
|
||||
# Find the document associated with this tab
|
||||
document = next((d for d in self._state.elements if d.tab_id == tab_id), None)
|
||||
|
||||
if document is None:
|
||||
raise ValueError(f"No document found for tab {tab_id}")
|
||||
|
||||
# Recreate the DataGrid with its saved state
|
||||
dg = DataGrid(self, _id=document.datagrid_id) # reload the state & settings
|
||||
dg = DataGrid(self, _id=document.datagrid_id)
|
||||
return dg
|
||||
|
||||
def clear_tree(self):
|
||||
@@ -324,106 +298,22 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
||||
self._tree.clear()
|
||||
return self._tree
|
||||
|
||||
# === DatagridMetadataProvider ===
|
||||
|
||||
def list_tables(self):
|
||||
return self._registry.get_all_tables()
|
||||
|
||||
def list_columns(self, table_name):
|
||||
return self._registry.get_columns(table_name)
|
||||
|
||||
def list_column_values(self, table_name, column_name):
|
||||
return self._registry.get_column_values(table_name, column_name)
|
||||
|
||||
def get_row_count(self, table_name):
|
||||
return self._registry.get_row_count(table_name)
|
||||
|
||||
def get_column_type(self, table_name, column_name):
|
||||
return self._registry.get_column_type(table_name, column_name)
|
||||
|
||||
def list_style_presets(self) -> list[str]:
|
||||
return list(self.style_presets.keys())
|
||||
|
||||
def list_format_presets(self) -> list[str]:
|
||||
return list(self.formatter_presets.keys())
|
||||
|
||||
def _resolve_store_for_table(self, table_name: str):
|
||||
"""
|
||||
Resolve the DatagridStore for a given table name.
|
||||
|
||||
Used by FormulaEngine as the registry_resolver callback.
|
||||
|
||||
Args:
|
||||
table_name: Full table name in ``"namespace.name"`` format.
|
||||
|
||||
Returns:
|
||||
DatagridStore instance or None if not found.
|
||||
"""
|
||||
try:
|
||||
as_fullname_dict = self._registry._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict.get(table_name)
|
||||
if grid_id is None:
|
||||
return None
|
||||
datagrid = InstancesManager.get(self._session, grid_id, None)
|
||||
if datagrid is None:
|
||||
return None
|
||||
return datagrid._df_store
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_style_presets(self) -> dict:
|
||||
"""Get the global style presets."""
|
||||
return self.style_presets
|
||||
|
||||
def get_formatter_presets(self) -> dict:
|
||||
"""Get the global formatter presets."""
|
||||
return self.formatter_presets
|
||||
|
||||
def get_formula_engine(self) -> FormulaEngine:
|
||||
"""The FormulaEngine shared across all DataGrids in this session."""
|
||||
return self._formula_engine
|
||||
|
||||
def add_style_preset(self, name: str, preset: dict):
|
||||
"""
|
||||
Add or update a style preset.
|
||||
|
||||
Args:
|
||||
name: Preset name (e.g., "custom_highlight")
|
||||
preset: Dict with CSS properties (e.g., {"background-color": "yellow", "color": "black"})
|
||||
"""
|
||||
self.style_presets[name] = preset
|
||||
|
||||
def add_formatter_preset(self, name: str, preset: dict):
|
||||
"""
|
||||
Add or update a formatter preset.
|
||||
|
||||
Args:
|
||||
name: Preset name (e.g., "custom_currency")
|
||||
preset: Dict with formatter config (e.g., {"type": "number", "prefix": "CHF ", "precision": 2})
|
||||
"""
|
||||
self.formatter_presets[name] = preset
|
||||
|
||||
def remove_style_preset(self, name: str):
|
||||
"""Remove a style preset."""
|
||||
if name in self.style_presets:
|
||||
del self.style_presets[name]
|
||||
|
||||
def remove_formatter_preset(self, name: str):
|
||||
"""Remove a formatter preset."""
|
||||
if name in self.formatter_presets:
|
||||
del self.formatter_presets[name]
|
||||
|
||||
# === UI ===
|
||||
# ------------------------------------------------------------------
|
||||
# UI
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def mk_main_icons(self):
|
||||
return Div(
|
||||
mk.icon(folder_open20_regular, tooltip="Upload from source", command=self.commands.upload_from_source()),
|
||||
mk.icon(table_add20_regular, tooltip="New grid", command=self.commands.new_grid()),
|
||||
mk.icon(folder_open20_regular, tooltip="Upload from source",
|
||||
command=self.commands.upload_from_source()),
|
||||
mk.icon(table_add20_regular, tooltip="New grid",
|
||||
command=self.commands.new_grid()),
|
||||
cls="flex"
|
||||
)
|
||||
|
||||
def _mk_tree(self):
|
||||
conf = TreeViewConf(add_leaf=False, icons={"folder": "database20_regular", "excel": "table20_regular"})
|
||||
conf = TreeViewConf(add_leaf=False,
|
||||
icons={"folder": "database20_regular", "excel": "table20_regular"})
|
||||
tree = TreeView(self, conf=conf, _id="-treeview")
|
||||
for element in self._state.elements:
|
||||
parent_id = tree.ensure_path(element.namespace, node_type="folder")
|
||||
|
||||
@@ -293,6 +293,9 @@ class TreeView(MultipleInstance):
|
||||
return self._state.items[node_id].bag
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def get_state(self) -> TreeViewState:
|
||||
return self._state
|
||||
|
||||
def _toggle_node(self, node_id: str):
|
||||
"""Toggle expand/collapse state of a node."""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from myfasthtml.core.constants import ColumnType, DATAGRID_DEFAULT_COLUMN_WIDTH, ViewType
|
||||
from myfasthtml.core.constants import DATAGRID_DEFAULT_COLUMN_WIDTH, ViewType, ROW_SELECTION_ID, ColumnType
|
||||
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataGridRowState:
|
||||
class DataGridRowUiState:
|
||||
row_id: int
|
||||
visible: bool = True
|
||||
height: int | None = None
|
||||
@@ -12,19 +13,32 @@ class DataGridRowState:
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataGridColumnState:
|
||||
col_id: str # name of the column: cannot be changed
|
||||
col_index: int # index of the column in the dataframe: cannot be changed
|
||||
title: str = None
|
||||
type: ColumnType = ColumnType.Text
|
||||
class DataGridColumnUiState:
|
||||
"""UI presentation state of a DataGrid column.
|
||||
|
||||
Holds only the visual properties of a column. Keyed by col_id to match
|
||||
the corresponding ColumnDefinition held by DataService.
|
||||
|
||||
Attributes:
|
||||
col_id: Column identifier. Must match the col_id in ColumnDefinition.
|
||||
width: Column width in pixels.
|
||||
visible: Whether the column is displayed.
|
||||
format: List of format rules applied during rendering.
|
||||
"""
|
||||
|
||||
col_id: str
|
||||
visible: bool = True
|
||||
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
|
||||
format: list = field(default_factory=list) #
|
||||
formula: str = "" # formula expression for ColumnType.Formula columns
|
||||
format: list = field(default_factory=list)
|
||||
|
||||
def copy(self):
|
||||
props = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||
return DataGridColumnState(**props)
|
||||
def copy(self) -> "DataGridColumnUiState":
|
||||
"""Return a shallow copy of this state."""
|
||||
return DataGridColumnUiState(
|
||||
col_id=self.col_id,
|
||||
width=self.width,
|
||||
visible=self.visible,
|
||||
format=list(self.format),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -56,4 +70,71 @@ class DataGridHeaderFooterConf:
|
||||
class DatagridView:
|
||||
name: str
|
||||
type: ViewType = ViewType.Table
|
||||
columns: list[DataGridColumnState] = None
|
||||
columns: list[DataGridColumnUiState] = None
|
||||
|
||||
|
||||
class DataGridColumnState:
|
||||
def __init__(self, col_def: ColumnDefinition, col_ui_state: DataGridColumnUiState):
|
||||
self._col_def = col_def
|
||||
self._col_ui_state = col_ui_state
|
||||
|
||||
@property
|
||||
def col_id(self):
|
||||
return self._col_def.col_id
|
||||
|
||||
@property
|
||||
def col_index(self):
|
||||
return self._col_def.col_index
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._col_def.title
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._col_def.type
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
return self._col_ui_state.visible
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._col_ui_state.width
|
||||
|
||||
@property
|
||||
def format(self):
|
||||
return self._col_ui_state.format
|
||||
|
||||
|
||||
class DataGridRowSelectionColumnState(DataGridColumnState):
|
||||
def __init__(self):
|
||||
super().__init__(None, None)
|
||||
|
||||
@property
|
||||
def col_id(self):
|
||||
return ROW_SELECTION_ID
|
||||
|
||||
@property
|
||||
def col_index(self):
|
||||
return -1
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return ""
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return ColumnType.RowSelection_
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
return True # not used
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 24 # not used
|
||||
|
||||
@property
|
||||
def format(self):
|
||||
return ""
|
||||
|
||||
@@ -37,75 +37,10 @@ class DataGridsRegistry(SingleInstance):
|
||||
del all_entries[datagrid_id]
|
||||
self._db_manager.save(DATAGRIDS_REGISTRY_ENTRY_KEY, all_entries)
|
||||
|
||||
def get_all_tables(self):
|
||||
all_entries = self._get_all_entries()
|
||||
return [f"{namespace}.{name}" for (namespace, name) in all_entries.values()]
|
||||
|
||||
def get_columns(self, table_name):
|
||||
try:
|
||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict[table_name]
|
||||
|
||||
# load datagrid state
|
||||
state_id = f"{grid_id}#state"
|
||||
state = self._db_manager.load(state_id)
|
||||
return [c.col_id for c in state["columns"]] if state else []
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def get_column_values(self, table_name, column_name):
|
||||
try:
|
||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict[table_name]
|
||||
|
||||
# load dataframe from dedicated store
|
||||
store = self._db_manager.load(f"{grid_id}#df")
|
||||
df = store["ne_df"] if store else None
|
||||
return df[column_name].tolist() if df is not None else []
|
||||
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def get_row_count(self, table_name):
|
||||
try:
|
||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict[table_name]
|
||||
|
||||
# load dataframe from dedicated store
|
||||
store = self._db_manager.load(f"{grid_id}#df")
|
||||
df = store["ne_df"] if store else None
|
||||
return len(df) if df is not None else 0
|
||||
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
def get_column_type(self, table_name, column_name):
|
||||
"""
|
||||
Get the type of a column.
|
||||
def get_all_entries(self) -> dict:
|
||||
"""Return all registry entries as {datagrid_id: (namespace, name)}."""
|
||||
return self._get_all_entries()
|
||||
|
||||
Args:
|
||||
table_name: The DataGrid name
|
||||
column_name: The column name
|
||||
|
||||
Returns:
|
||||
ColumnType enum value or None if not found
|
||||
"""
|
||||
try:
|
||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||
grid_id = as_fullname_dict[table_name]
|
||||
|
||||
# load datagrid state
|
||||
state_id = f"{grid_id}#state"
|
||||
state = self._db_manager.load(state_id)
|
||||
|
||||
if state and "columns" in state:
|
||||
for col in state["columns"]:
|
||||
if col.col_id == column_name:
|
||||
return col.type
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _get_all_entries(self):
|
||||
return {k: v for k, v in self._db_manager.load(DATAGRIDS_REGISTRY_ENTRY_KEY).items()
|
||||
if not k.startswith("__")}
|
||||
|
||||
@@ -82,6 +82,7 @@ class DataService(MultipleInstance):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = DataServiceState(self, save_state=save_state)
|
||||
self._store = DataStore(self, save_state=save_state)
|
||||
self._init_store()
|
||||
|
||||
@property
|
||||
def columns(self) -> list[ColumnDefinition]:
|
||||
@@ -128,12 +129,7 @@ class DataService(MultipleInstance):
|
||||
self._state.columns = self._build_column_definitions(df)
|
||||
self._state.save()
|
||||
|
||||
self._store.ns_fast_access = self._build_fast_access(df)
|
||||
self._store.ns_row_data = df.to_dict(orient="records")
|
||||
self._store.ns_total_rows = len(df)
|
||||
self._store.save()
|
||||
|
||||
self._register_existing_formulas()
|
||||
self._init_store()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Mutations
|
||||
@@ -302,6 +298,16 @@ class DataService(MultipleInstance):
|
||||
# Private helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _init_store(self):
|
||||
df = self._store.ne_df
|
||||
if df is None:
|
||||
return
|
||||
self._store.ns_fast_access = self._build_fast_access(df)
|
||||
self._store.ns_row_data = df.to_dict(orient="records")
|
||||
self._store.ns_total_rows = len(df)
|
||||
self._store.save()
|
||||
self._register_existing_formulas()
|
||||
|
||||
def _build_column_definitions(self, df: pd.DataFrame) -> list[ColumnDefinition]:
|
||||
"""Build ColumnDefinition objects from DataFrame columns.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user