commit 43abaaa91da3b19567afbd38551cefee92980289
parent b35185b2cfa3ae30f20c24403ced0125d55e40b2
Author: Andy Khramtsov <>
Date: Wed, 27 May 2026 17:31:45 +0300
feat: add table
Diffstat:
3 files changed, 206 insertions(+), 7 deletions(-)
diff --git a/src/deps/aio_components/__init__.py b/src/deps/aio_components/__init__.py
@@ -0,0 +1 @@
+from . import table_aio as table_aio
diff --git a/src/deps/aio_components/table_aio.py b/src/deps/aio_components/table_aio.py
@@ -0,0 +1,152 @@
+import uuid
+from typing import Any
+
+import dash_ag_grid as dag
+from dash import MATCH, Input, Output, callback, dcc, html
+
+
+class TableAIO(html.Div):
+ class ids:
+ def ag_grid(aio_id: Any):
+ return {
+ "component": "TableAIO",
+ "subcomponent": "ag_grid",
+ "aio_id": aio_id,
+ }
+
+ def download_csv_button(aio_id: Any):
+ return {
+ "component": "TableAIO",
+ "subcomponent": "download_csv_button",
+ "aio_id": aio_id,
+ }
+
+ def clear_column_state_button(aio_id: Any):
+ return {
+ "component": "TableAIO",
+ "subcomponent": "clear_column_state_button",
+ "aio_id": aio_id,
+ }
+
+ def clear_filters_button(aio_id: Any):
+ return {
+ "component": "TableAIO",
+ "subcomponent": "clear_filters_button",
+ "aio_id": aio_id,
+ }
+
+ def size_to_fit_button(aio_id: Any):
+ return {
+ "component": "TableAIO",
+ "subcomponent": "size_to_fit_button",
+ "aio_id": aio_id,
+ }
+
+ ids = ids
+
+ def __init__(self, aio_id=None, csv_filename=None, column_defs=None, row_data=None):
+ if column_defs is None:
+ column_defs = []
+ if aio_id is None:
+ aio_id = str(uuid.uuid4())
+ if csv_filename is None:
+ csv_filename = aio_id
+ if row_data is None:
+ row_data = []
+
+ # todo: add props customization
+
+ super().__init__(
+ className="vertical-content",
+ children=[
+ dag.AgGrid(
+ id=self.ids.ag_grid(aio_id),
+ rowData=row_data,
+ columnDefs=column_defs,
+ dashGridOptions={
+ "pagination": True,
+ "paginationPageSize": 10,
+ "paginationPageSizeSelector": [10, 20, 50, 100],
+ "enableCellTextSelection": True,
+ "ensureDomOrder": True,
+ "maintainColumnOrder": True,
+ "rowSelection": {"mode": "multiRow"},
+ "animateRows": False,
+ "suppressColumnMoveAnimation": True,
+ "domLayout": "autoHeight",
+ },
+ style={"height": None},
+ persistence=True,
+ persistence_type="session",
+ defaultColDef={"filter": True, "cellDataType": False},
+ persisted_props=["columnState", "selectedRows", "filterModel"],
+ csvExportParams={
+ "fileName": f"{csv_filename}.csv",
+ },
+ ),
+ html.Div(
+ className="horizontal-content horizontal-content_small-gap",
+ children=[
+ dcc.Button(
+ "Download CSV",
+ id=self.ids.download_csv_button(aio_id),
+ className="button",
+ n_clicks=0,
+ ),
+ dcc.Button(
+ "Clear column state",
+ id=self.ids.clear_column_state_button(aio_id),
+ className="button",
+ ),
+ dcc.Button(
+ "Clear filters",
+ id=self.ids.clear_filters_button(aio_id),
+ className="button",
+ ),
+ dcc.Button(
+ "Size to fit",
+ id=self.ids.size_to_fit_button(aio_id),
+ className="button",
+ ),
+ ],
+ ),
+ ],
+ )
+
+ @callback(
+ Output(component_id=ids.ag_grid(MATCH), component_property="exportDataAsCsv"),
+ Input(component_id=ids.download_csv_button(MATCH), component_property="n_clicks"),
+ prevent_initial_call=True,
+ )
+ def export_csv(n_clicks):
+ return bool(n_clicks)
+
+ @callback(
+ Output(component_id=ids.ag_grid(MATCH), component_property="resetColumnState"),
+ Input(
+ component_id=ids.clear_column_state_button(MATCH),
+ component_property="n_clicks",
+ ),
+ prevent_initial_call=True,
+ )
+ def reset_columns(n_clicks):
+ return True
+
+ @callback(
+ Output(component_id=ids.ag_grid(MATCH), component_property="filterModel"),
+ Input(component_id=ids.clear_filters_button(MATCH), component_property="n_clicks"),
+ prevent_initial_call=True,
+ )
+ def reset_filters(n_clicks):
+ return {}
+
+ @callback(
+ Output(component_id=ids.ag_grid(MATCH), component_property="columnSize"),
+ Input(
+ component_id=ids.size_to_fit_button(MATCH),
+ component_property="n_clicks",
+ ),
+ prevent_initial_call=True,
+ )
+ def size_to_fit(n_clicks):
+ return "sizeToFit"
diff --git a/src/deps/pages/home.py b/src/deps/pages/home.py
@@ -1,24 +1,47 @@
import base64
+import tomllib
import dash
+import polars as pl
from dash import Input, Output, State, callback, dcc, html
from dash.exceptions import PreventUpdate
+from deps.aio_components.table_aio import TableAIO
+
dash.register_page(__name__, path="/", order=1)
+class ids:
+ cargo_lock_textarea: str = "cargo-lock-textarea"
+ upload_cargo_lock: str = "upload-cargo-lock"
+ cargo_lock_table: str = "cargo-lock-table"
+ cargo_lock_filename_display: str = "cargo-lock-filename-display"
+
+
def layout():
return html.Div(
className="padded-box vertical-content vertical-content_large-gap",
children=[
html.H2("Cargo dependency visualisation"),
- dcc.Textarea(id="text-content", placeholder="Cargo.lock contents", style={}),
+ dcc.Textarea(id=ids.cargo_lock_textarea, placeholder="Cargo.lock contents", style={}),
dcc.Upload(
- id="upload-file",
+ id=ids.upload_cargo_lock,
children=[
dcc.Button("Upload Cargo.lock"),
html.Div("Uploaded file name:"),
- html.Div(id="upload-file-filename"),
+ html.Div(id=ids.cargo_lock_filename_display, children="Empty"),
+ ],
+ ),
+ TableAIO(
+ aio_id=ids.cargo_lock_table,
+ column_defs=[
+ {"field": i, "colId": i}
+ for i in [
+ "name",
+ "version",
+ "checksum",
+ "dependencies",
+ ]
],
),
],
@@ -27,14 +50,14 @@ def layout():
@callback(
dict(
- textarea=Output("text-content", "value"),
- filename=Output("upload-file-filename", "children"),
+ textarea=Output(ids.cargo_lock_textarea, "value"),
+ filename=Output(ids.cargo_lock_filename_display, "children"),
),
dict(
- upload_contents=Input("upload-file", "contents"),
+ upload_contents=Input(ids.upload_cargo_lock, "contents"),
),
dict(
- upload_filename=State("upload-file", "filename"),
+ upload_filename=State(ids.upload_cargo_lock, "filename"),
),
)
def upload_cargo_lock(inputs, state):
@@ -47,3 +70,26 @@ def upload_cargo_lock(inputs, state):
textarea=decoded.decode("utf-8"),
filename=state["upload_filename"],
)
+
+
+@callback(
+ dict(
+ table_rows=Output(TableAIO.ids.ag_grid(ids.cargo_lock_table), "rowData"),
+ ),
+ dict(
+ textarea=Input(ids.cargo_lock_textarea, "value"),
+ ),
+ dict(
+ upload_filename=State(ids.upload_cargo_lock, "filename"),
+ ),
+)
+def visualize(inputs, state):
+ try:
+ toml = tomllib.loads(inputs["textarea"])
+ except tomllib.TOMLDecodeError as error:
+ raise PreventUpdate from error
+ df = pl.DataFrame(toml.get("package", []))
+ print(df)
+ return dict(
+ table_rows=df.to_dicts(),
+ )