From 612df0a8ebf6b4372de772acbee8fd10955aa042 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Sun, 28 Jul 2024 17:34:56 +0200 Subject: [PATCH] Feat: callback to toggle editing --- dashboard/app.py | 6 +- dashboard/libs/stage/fs_stage.py | 12 ++- dashboard/pages/table.py | 128 +++++++++++++++++++++++++++---- 3 files changed, 127 insertions(+), 19 deletions(-) diff --git a/dashboard/app.py b/dashboard/app.py index 971fd8a..1fcc57f 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -7,7 +7,11 @@ external_scripts = [ ] # external_script = ["https://tailwindcss.com/", {"src": "https://cdn.tailwindcss.com"}] -app = Dash(__name__, use_pages=True, external_scripts=external_scripts) +app = Dash(__name__, + use_pages=True, + external_scripts=external_scripts, + suppress_callback_exceptions=True, + ) app.scripts.config.serve_locally = True dash.register_page( home.__name__, diff --git a/dashboard/libs/stage/fs_stage.py b/dashboard/libs/stage/fs_stage.py index 631487e..a22067b 100644 --- a/dashboard/libs/stage/fs_stage.py +++ b/dashboard/libs/stage/fs_stage.py @@ -1,5 +1,6 @@ from .stage import AbstractStage from pathlib import Path +import pandas as pd class FSStage(AbstractStage): def __init__(self, name, basepath, metadata_engine=None): @@ -40,9 +41,16 @@ class FSStage(AbstractStage): table_path = self.build_table_path(table, schema) pass - def read(self, table:str, schema:str='.'): + def read(self, table:str, schema:str='.', read_options={}): table_path = self.build_table_path(table, schema) - pass + extension = table_path.suffix + if extension == '.csv': + return pd.read_csv(table_path, **read_options) + + if extension == '.xlsx': + return pd.read_excel(table_path, **read_options) + + raise ValueError("Can't open the table") def write(self, table:str, content, schema:str='.'): table_path = self.build_table_path(table, schema) diff --git a/dashboard/pages/table.py b/dashboard/pages/table.py index be11baf..782f5ad 100644 --- a/dashboard/pages/table.py +++ b/dashboard/pages/table.py @@ -1,30 +1,126 @@ -from dash import html, dcc +from dash import html, dcc, dash_table, callback, Input, Output, State +from dash.exceptions import PreventUpdate from ..datalake import stages from ..libs.stage.stage import AbstractStage def layout(stage_name=None, schema_name=None, table_name=None): stage = stages[stage_name] + df = stage.read(table=table_name, schema=schema_name) return html.Div([ - html.H2([ - dcc.Link( - f"{stage.name}", - href=f"/stage/{stage.name}", - className="hover:underline" + dcc.Store(id="table_backup"), + html.Div([ + html.H2([ + dcc.Link( + f"{stage.name}", + href=f"/stage/{stage.name}", + className="hover:underline" + ), + html.Span(" > "), + dcc.Link( + f"{schema_name}", + href=f"/stg/{stage.name}/schema/{schema_name}", + className="hover:underline" + ), + html.Span(" > "), + html.Span(table_name), + ], + className="text-2xl" ), - html.Span(" > "), - dcc.Link( - f"{schema_name}", - href=f"/stg/{stage.name}/schema/{schema_name}", - className="hover:underline" + html.Div([ + html.Button( + "Editer", + id="btn_edit", + className="rounded border px-2 py-1", + style={"display": "block"} + ), + html.Button( + "Sauver", + id="btn_save", + className="rounded border px-2 py-1 border-green-500 hover:bg-green-500", + style={"display": "none"} + ), + html.Button( + "Annuler", + id="btn_cancel", + className="rounded border px-2 py-1 border-red-500 hover:bg-red-500", + style={"display": "none"} + ), + ], + className="flex flex-row space-x-2", + id="toolbar" ), - html.Span(" > "), - html.Span(table_name), ], - className="text-2xl p-4 py-2" - + className="flex flex-row justify-between p-4" ), - ]) + html.Div([ + html.Div([ + dash_table.DataTable( + id="datatable", + data=df.to_dict('records'), + columns=[{"name": i, "id": i} for i in df.columns], + filter_action="native", + sort_action="native", + sort_mode="multi", + editable=False + ) + ]) + ], + className="overflow-y-auto" + ), + ], + className="p-2" + ) +@callback( + Output("datatable", 'editable', allow_duplicate=True), + Output("table_backup", 'data'), + Input("btn_edit", "n_clicks"), + State("datatable", 'data'), + prevent_initial_call=True +) +def activate_editable(n_clicks, df_src): + if n_clicks is None: + raise PreventUpdate + if n_clicks > 0: + df_backup = df_src.copy() + return True, df_backup + raise PreventUpdate +@callback( + Output("datatable", 'editable'), + Output("datatable", 'data'), + Input("btn_cancel", "n_clicks"), + State("table_backup", 'data'), + prevent_initial_call=True +) +def cancel_modifications(n_clicks, data): + if n_clicks is None: + raise PreventUpdate + if n_clicks > 0 and data is not None: + return False, data.copy() + raise PreventUpdate + +# @callback( +# Output("datatable", 'editable'), +# Input("btn_save", "n_clicks"), +# State("datatable", 'editable'), +# ) +# def save_modifications(n_clicks, editable): +# if n_clicks is None: +# raise PreventUpdate +# if n_clicks > 0: +# return not editable +# return editable + +@callback( + Output("btn_edit", "style"), + Output("btn_save", "style"), + Output("btn_cancel", "style"), + Input("datatable", "editable"), +) +def toolbar(editable): + if editable: + return {"display": "none"}, {"display": "block"}, {"display": "block"} + return {"display": "block"}, {"display": "none"}, {"display": "none"}