rsdeps

Cargo.lock visualizer (mirror)
Log | Files | Refs | README | LICENSE

commit 43abaaa91da3b19567afbd38551cefee92980289
parent b35185b2cfa3ae30f20c24403ced0125d55e40b2
Author: Andy Khramtsov <>
Date:   Wed, 27 May 2026 17:31:45 +0300

feat: add table

Diffstat:
Asrc/deps/aio_components/__init__.py | 1+
Asrc/deps/aio_components/table_aio.py | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/deps/pages/home.py | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++-------
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(), + )