From d25a0f835c2c5a7572ea73f0e6aea3bf1b337cb5 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 09:11:01 +0000 Subject: [PATCH 01/12] use table_widget --- calculation_history.ipynb | 9 +- src/aiidalab_qe/app/utils/search_jobs.py | 100 +++++++++++------------ 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index c917f7205..1ed0b326d 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -119,11 +119,18 @@ "source": [ "calculation_history.table" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 7606367d9..60c3e9f88 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -1,16 +1,38 @@ import ipywidgets as ipw import pandas as pd from IPython.display import display +from table_widget import TableWidget from aiida.orm import QueryBuilder -state_icons = { +STATE_ICONS = { "running": "⏳", "finished": "✅", "excepted": "⚠️", "killed": "❌", } +COLUMNS = { + "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, + "Creation time": { + "headerName": "Creation Time ⏰", + "width": 150, + "editable": False, + }, + "Structure": {"headerName": "Structure", "editable": False}, + "State": {"headerName": "State 🟢", "editable": False}, + "Label": {"headerName": "Label", "width": 300, "editable": True}, + "Description": { + "headerName": "Description", + "width": 300, + "editable": True, + "hide": True, + }, + "Relax_type": {"headerName": "Relax_type", "editable": False, "hide": True}, + "Delete": {"headerName": "Delete", "dataType": "link", "editable": False}, + "Download": {"headerName": "Download", "dataType": "link", "editable": False}, +} + class QueryInterface: def __init__(self): @@ -18,7 +40,8 @@ def __init__(self): def setup_table(self): self.df = self.load_data() - self.table = ipw.HTML() + self.table = TableWidget() + self.setup_widgets() def load_data(self): @@ -142,24 +165,6 @@ def setup_widgets(self): value="", # Default value corresponding to "Any" description="Job State:", ) - self.label_search_field = ipw.Text( - value="", - placeholder="Enter a keyword", - description="", - disabled=False, - style={"description_width": "initial"}, - ) - self.label_search_description = ipw.HTML( - "

Search Label: Enter a keyword to search in both the Label and Description fields. Matches will include any calculations where the keyword is found in either field.

" - ) - self.toggle_description_checkbox = ipw.Checkbox( - value=False, # Show the Description column by default - description="Show Description", - indent=False, - ) - self.toggle_description_checkbox.observe( - self.update_table_visibility, names="value" - ) self.toggle_time_format = ipw.ToggleButtons( options=["Absolute", "Relative"], value="Absolute", # Default to Absolute time @@ -172,6 +177,15 @@ def setup_widgets(self): description="ID Format:", ) self.toggle_id_format.observe(self.update_table_visibility, names="value") + self.toggle_multi_selection = ipw.ToggleButtons( + options=["On", "Off"], + value="Off", + description="Checkbox selection", + tooltips=["Enable multiple selection.", "Disable multiple selection"], + ) + self.toggle_multi_selection.observe( + self.update_table_configuration, names="value" + ) self.time_start = ipw.DatePicker(description="Start Time:") self.time_end = ipw.DatePicker(description="End Time:") @@ -183,7 +197,6 @@ def setup_widgets(self): self.time_start.observe(self.apply_filters, names="value") self.time_end.observe(self.apply_filters, names="value") self.job_state_dropdown.observe(self.apply_filters, names="value") - self.label_search_field.observe(self.apply_filters, names="value") self.filters_layout = ipw.VBox( [ @@ -192,12 +205,6 @@ def setup_widgets(self): [ self.job_state_dropdown, self.time_box, - ipw.HBox( - [ - self.label_search_description, - self.label_search_field, - ] - ), ipw.VBox( [self.properties_filter_description, self.properties_box] ), @@ -207,9 +214,9 @@ def setup_widgets(self): ipw.HTML("

Display Options:

"), ipw.VBox( [ - self.toggle_description_checkbox, self.toggle_time_format, self.toggle_id_format, + self.toggle_multi_selection, ] ), ] @@ -230,10 +237,6 @@ def get_table_value(self, display_df): display_df["Creation time"] = display_df["ctime"].apply( lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if pd.notnull(x) else "N/A" ) - # Conditionally drop the Description column based on the checkbox state - if not self.toggle_description_checkbox.value: - display_df = display_df.drop(columns=["Description"]) - # Adjust the ID column based on the toggle state if self.toggle_id_format.value == "PK": display_df = display_df.rename(columns={"PK_with_link": "ID"}).drop( @@ -246,20 +249,15 @@ def get_table_value(self, display_df): # display_df["State"] = display_df["State"].apply( - lambda x: f"{x.capitalize()}{state_icons.get(x.lower())}" - ) - display_df.rename( - columns={ - "State": "State 🟢", - "Creation time": "Creation Time ⏰", - "ID": "ID 🔗", - }, - inplace=True, + lambda x: f"{x.capitalize()}{STATE_ICONS.get(x.lower())}" ) display_df = display_df.drop(columns=["Properties", "ctime"]) - self.table.value = self.css_style + display_df.to_html( - classes="df", escape=False, index=False - ) + columns = [] + for col in display_df.columns: + column = COLUMNS.get(col) + column["field"] = col + columns.append(COLUMNS[col]) + self.table.from_data(display_df, columns=columns) def apply_filters(self, _): selected_properties = [ @@ -269,15 +267,6 @@ def apply_filters(self, _): filtered_df = filtered_df[ filtered_df["State"].str.contains(self.job_state_dropdown.value) ] - if self.label_search_field.value: - filtered_df = filtered_df[ - filtered_df["Label"].str.contains( - self.label_search_field.value, case=False, na=False - ) - | filtered_df["Description"].str.contains( - self.label_search_field.value, case=False, na=False - ) - ] if selected_properties: filtered_df = filtered_df[ filtered_df["Properties"].apply( @@ -301,6 +290,11 @@ def update_table_visibility(self, _): # Reapply filters to refresh the table visibility when the checkbox changes self.apply_filters(None) + def update_table_configuration(self, _): + enable = True if self.toggle_multi_selection.value == "On" else False + config = {"pagination": True, "checkboxSelection": enable} + self.table.config = config + def display(self): display(self.filters_layout) display(self.table) From 748706151ddfc6e8c85bd44272059b5a5b3d8e04 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 12:15:38 +0000 Subject: [PATCH 02/12] use `on_row_update` to update label and description --- calculation_history.ipynb | 16 ++--- src/aiidalab_qe/app/utils/search_jobs.py | 91 +++++++++++------------- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 92ce9a085..1dc0b6ca5 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -104,11 +104,10 @@ "metadata": {}, "outputs": [], "source": [ - "from aiidalab_qe.app.utils.search_jobs import QueryInterface\n", + "from aiidalab_qe.app.utils.search_jobs import CalculationHistory\n", "\n", - "calculation_history = QueryInterface()\n", - "calculation_history.setup_table()\n", - "calculation_history.filters_layout" + "calculation_history = CalculationHistory()\n", + "calculation_history.main" ] }, { @@ -117,15 +116,8 @@ "metadata": {}, "outputs": [], "source": [ - "calculation_history.table" + "calculation_history.load_table()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index b8705727e..148a5ffc7 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -1,9 +1,9 @@ import ipywidgets as ipw import pandas as pd -from IPython.display import display from table_widget import TableWidget -from aiida.orm import QueryBuilder +from aiida.orm import QueryBuilder, load_node +from aiidalab_qe.common.widgets import LoadingWidget STATE_ICONS = { "running": "⏳", @@ -16,6 +16,7 @@ "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, "Creation time": { "headerName": "Creation Time ⏰", + # "type": "date", "width": 150, "editable": False, }, @@ -31,17 +32,25 @@ "Relax_type": {"headerName": "Relax_type", "editable": False, "hide": True}, "Delete": {"headerName": "Delete", "dataType": "link", "editable": False}, "Download": {"headerName": "Download", "dataType": "link", "editable": False}, + "UUID": {"headerName": "UUID", "editable": False, "hide": True}, } -class QueryInterface: +class CalculationHistory: def __init__(self): - pass + self.main = ipw.VBox(children=[LoadingWidget("Loading the table...")]) - def setup_table(self): - self.df = self.load_data() self.table = TableWidget() + def on_row_update(change): + node = load_node(change["new"]["UUID"]) + node.label = change["new"]["Label"] + node.description = change["new"]["Description"] + + self.table.observe(on_row_update, "updatedRow") + + def load_table(self): + self.df = self.load_data() self.setup_widgets() def load_data(self): @@ -114,6 +123,7 @@ def load_data(self): "Label", "Description", "Relax_type", + "UUID", "Delete", "Download", "Properties", @@ -122,17 +132,6 @@ def load_data(self): ] def setup_widgets(self): - self.css_style = """ - - """ - unique_properties = set(self.df["Properties"].explode().dropna()) unique_properties.discard(None) property_checkboxes = [ @@ -198,45 +197,41 @@ def setup_widgets(self): self.time_end.observe(self.apply_filters, names="value") self.job_state_dropdown.observe(self.apply_filters, names="value") - self.filters_layout = ipw.VBox( - [ - ipw.HTML("

Search & filter calculations:

"), - ipw.VBox( - [ - self.job_state_dropdown, - self.time_box, - ipw.VBox( - [self.properties_filter_description, self.properties_box] - ), - # self.apply_filters_btn, - ] - ), - ipw.HTML("

Display options:

"), - ipw.VBox( - [ - self.toggle_time_format, - self.toggle_id_format, - self.toggle_multi_selection, - ] - ), - ] - ) - self.get_table_value(self.df) + self.main.children = [ + ipw.HTML("

Filters:

"), + ipw.VBox( + [ + self.job_state_dropdown, + self.time_box, + ipw.VBox([self.properties_filter_description, self.properties_box]), + # self.apply_filters_btn, + ] + ), + ipw.HTML("

Display options:

"), + ipw.VBox( + [ + self.toggle_time_format, + self.toggle_id_format, + self.toggle_multi_selection, + ] + ), + self.table, + ] + self.update_table_value(self.df) - def get_table_value(self, display_df): - if display_df.empty: - self.table.value = "

No results found

" - return + def update_table_value(self, display_df): # Adjust the Creation time column based on the toggle state if self.toggle_time_format.value == "Relative": now = pd.Timestamp.now(tz="UTC") display_df["Creation time"] = display_df["ctime"].apply( lambda x: f"{(now - x).days}D ago" if pd.notnull(x) else "N/A" ) + COLUMNS["Creation time"].pop("type", None) else: display_df["Creation time"] = display_df["ctime"].apply( lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if pd.notnull(x) else "N/A" ) + # COLUMNS["Creation time"]["type"] = "date" # Adjust the ID column based on the toggle state if self.toggle_id_format.value == "PK": display_df = display_df.rename(columns={"PK_with_link": "ID"}).drop( @@ -284,7 +279,7 @@ def apply_filters(self, _): (filtered_df["ctime"] >= start_time) & (filtered_df["ctime"] <= end_time) ] - self.get_table_value(filtered_df) + self.update_table_value(filtered_df) def update_table_visibility(self, _): # Reapply filters to refresh the table visibility when the checkbox changes @@ -294,7 +289,3 @@ def update_table_configuration(self, _): enable = True if self.toggle_multi_selection.value == "On" else False config = {"pagination": True, "checkboxSelection": enable} self.table.config = config - - def display(self): - display(self.filters_layout) - display(self.table) From 30e0629cba7d94f621498350bbe07ef29c8b1f14 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 14:13:18 +0000 Subject: [PATCH 03/12] arrange widgets and udpate guide --- calculation_history.ipynb | 36 ++++--- src/aiidalab_qe/app/utils/search_jobs.py | 128 ++++++++++++++--------- 2 files changed, 104 insertions(+), 60 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 1dc0b6ca5..17ad2cc30 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -50,24 +50,34 @@ " color: #2c3e50;\n", " }\n", " \n", - "
Calculation history
\n", + "
Calculation History
\n", "
\n", - "

How to use this page

\n", + "

How to Use This Page

\n", "

\n", - " This page allows you to view and manage your calculation history. Use the table below to\n", - " see all jobs in the database. You can use the following filters to narrow down your search:\n", + " This page allows you to view, filter, sort, and manage your calculation history.\n", "

\n", + "

Filters and Search

\n", "
    \n", - "
  • Label search field: Enter a job label to find matching jobs.
  • \n", - "
  • Job state dropdown: Filter jobs based on their state (e.g., finished, running).
  • \n", - "
  • Date range picker: Select a start and end date to view jobs created within that range.
  • \n", - "
  • Properties filter: Select specific properties associated with jobs.
  • \n", + "
  • Quick Search Field: Use the search bar above the table to find jobs by any visible field.
  • \n", + "
  • Column-Specific Filters: Filter data directly in each column using their respective filter options.
  • \n", + "
  • Job State Dropdown: Filter jobs by their state (e.g., Finished, Running).
  • \n", + "
  • Date Range Picker: Select a start and end date to narrow down jobs based on creation date.
  • \n", + "
  • Properties Filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.
  • \n", + "
\n", + "

Table Actions

\n", + "
    \n", + "
  • Editable Fields: Edit the label and description of a job directly by clicking on the respective fields.
  • \n", + "
  • Inspect: Click on a job's ID link to view detailed information.
  • \n", + "
  • Delete: Remove a job by clicking the \"Delete\" link in its row. A confirmation page will be opened.
  • \n", + "
  • Download: Download raw data (i.e. input and output files) and/or the AiiDA archive of a job using the \"Download\" link.
  • \n", + "
\n", + "

Display Options

\n", + "
    \n", + "
  • Sorting: Click any column header to sort the table by that column.
  • \n", + "
  • Column Management: Show, hide columns to customize the table view.
  • \n", + "
  • Time Format: Toggle between \"Absolute\" (specific dates) and \"Relative\" (time elapsed).
  • \n", + "
  • ID Format: Switch between \"PK\" or \"UUID\" for job identification.
  • \n", "
\n", - "

\n", - " Each row in the table provides links to inspect, delete or download a job. To delete a job, click the \"Delete\"\n", - " link in the respective row. To view detailed information about a job, click the \"PK\" link. To download the\n", - " input/output files of a job, click the \"Download\" link.\n", - "

\n", "
\n", " \"\"\"\n", ")\n", diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 148a5ffc7..bab78bb91 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -14,9 +14,14 @@ COLUMNS = { "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, - "Creation time": { - "headerName": "Creation Time ⏰", - # "type": "date", + "Creation time absolute": { + "headerName": "Creation Time ⏰ (absolute)", + "type": "date", + "width": 150, + "editable": False, + }, + "Creation time relative": { + "headerName": "Creation Time ⏰ (relative)", "width": 150, "editable": False, }, @@ -40,7 +45,7 @@ class CalculationHistory: def __init__(self): self.main = ipw.VBox(children=[LoadingWidget("Loading the table...")]) - self.table = TableWidget() + self.table = TableWidget(style={"margin-top": "20px"}) def on_row_update(change): node = load_node(change["new"]["UUID"]) @@ -87,9 +92,13 @@ def load_data(self): df = pd.DataFrame(results, columns=headers) # Check if DataFrame is not empty if not df.empty: - df["Creation time"] = df["ctime"].apply( + df["Creation time absolute"] = df["ctime"].apply( lambda x: x.strftime("%Y-%m-%d %H:%M:%S") ) + now = pd.Timestamp.now(tz="UTC") + df["Creation time relative"] = df["ctime"].apply( + lambda x: f"{(now - x).days}D ago" if pd.notnull(x) else "N/A" + ) df["Delete"] = df["PK"].apply( lambda pk: f'Delete' ) @@ -117,7 +126,8 @@ def load_data(self): [ "PK_with_link", "UUID_with_link", - "Creation time", + "Creation time absolute", + "Creation time relative", "Structure", "State", "Label", @@ -144,11 +154,10 @@ def setup_widgets(self): for prop in unique_properties ] self.properties_box = ipw.HBox( - children=property_checkboxes, description="Properties:" - ) - self.properties_filter_description = ipw.HTML( - "

Properties filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.

" + children=property_checkboxes, + indent=True, ) + self.properties_filter_description = ipw.HTML("Filter by properties:") # Replace 'None' in 'Properties' with an empty list self.df["Properties"] = self.df["Properties"].apply( lambda x: [] if x is None else x @@ -176,15 +185,15 @@ def setup_widgets(self): description="ID format:", ) self.toggle_id_format.observe(self.update_table_visibility, names="value") - self.toggle_multi_selection = ipw.ToggleButtons( - options=["On", "Off"], - value="Off", - description="Checkbox selection", - tooltips=["Enable multiple selection.", "Disable multiple selection"], - ) - self.toggle_multi_selection.observe( - self.update_table_configuration, names="value" - ) + # self.toggle_multi_selection = ipw.ToggleButtons( + # options=["On", "Off"], + # value="Off", + # description="Checkbox selection", + # tooltips=["Enable multiple selection.", "Disable multiple selection"], + # ) + # self.toggle_multi_selection.observe( + # self.update_table_configuration, names="value" + # ) self.time_start = ipw.DatePicker(description="Start time:") self.time_end = ipw.DatePicker(description="End time:") @@ -196,42 +205,67 @@ def setup_widgets(self): self.time_start.observe(self.apply_filters, names="value") self.time_end.observe(self.apply_filters, names="value") self.job_state_dropdown.observe(self.apply_filters, names="value") - - self.main.children = [ - ipw.HTML("

Filters:

"), - ipw.VBox( - [ - self.job_state_dropdown, - self.time_box, - ipw.VBox([self.properties_filter_description, self.properties_box]), - # self.apply_filters_btn, - ] + display_options = ipw.VBox( + children=[ + ipw.HTML("

Display options:

"), + ipw.VBox( + [ + self.toggle_time_format, + self.toggle_id_format, + # self.toggle_multi_selection, + ] + ), + ], + layout=ipw.Layout( + border="1px solid #ddd", + padding="0px", + margin="0px, 0px, 100px, 0px", + background_color="#f9f9f9", + border_radius="5px", + box_shadow="0 2px 5px rgba(0, 0, 0, 0.1)", ), - ipw.HTML("

Display options:

"), - ipw.VBox( - [ - self.toggle_time_format, - self.toggle_id_format, - self.toggle_multi_selection, - ] + ) + filters = ipw.VBox( + children=[ + ipw.HTML("

Filters:

"), + ipw.VBox( + [ + self.job_state_dropdown, + self.time_box, + ipw.VBox( + [self.properties_filter_description, self.properties_box], + layout=ipw.Layout( + border="1px solid #ddd", padding="5px", margin="5px" + ), + ), + ] + ), + ], + layout=ipw.Layout( + border="1px solid #ddd", + padding="0px", + margin="0px, 0px, 100px, 0px", + background_color="#f9f9f9", + border_radius="5px", + box_shadow="0 2px 5px rgba(0, 0, 0, 0.1)", ), + ) + + self.main.children = [ + display_options, + filters, self.table, ] self.update_table_value(self.df) def update_table_value(self, display_df): # Adjust the Creation time column based on the toggle state - if self.toggle_time_format.value == "Relative": - now = pd.Timestamp.now(tz="UTC") - display_df["Creation time"] = display_df["ctime"].apply( - lambda x: f"{(now - x).days}D ago" if pd.notnull(x) else "N/A" - ) - COLUMNS["Creation time"].pop("type", None) - else: - display_df["Creation time"] = display_df["ctime"].apply( - lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if pd.notnull(x) else "N/A" - ) - # COLUMNS["Creation time"]["type"] = "date" + COLUMNS["Creation time relative"]["hide"] = ( + self.toggle_time_format.value != "Relative" + ) + COLUMNS["Creation time absolute"]["hide"] = ( + self.toggle_time_format.value != "Absolute" + ) # Adjust the ID column based on the toggle state if self.toggle_id_format.value == "PK": display_df = display_df.rename(columns={"PK_with_link": "ID"}).drop( From 02a989c0dcc8d3a476e18eb1b82278819a05922f Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 15:12:07 +0000 Subject: [PATCH 04/12] implemented review resuggestion. - sentence case - add `children` - format lines --- calculation_history.ipynb | 28 ++++++++++++------------ src/aiidalab_qe/app/utils/search_jobs.py | 28 ++++++++++-------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 17ad2cc30..88d1525bd 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -50,33 +50,33 @@ " color: #2c3e50;\n", " }\n", " \n", - "
Calculation History
\n", + "
Calculation history
\n", "
\n", - "

How to Use This Page

\n", + "

How to use this page

\n", "

\n", " This page allows you to view, filter, sort, and manage your calculation history.\n", "

\n", - "

Filters and Search

\n", + "

Filters and search

\n", "
    \n", - "
  • Quick Search Field: Use the search bar above the table to find jobs by any visible field.
  • \n", - "
  • Column-Specific Filters: Filter data directly in each column using their respective filter options.
  • \n", - "
  • Job State Dropdown: Filter jobs by their state (e.g., Finished, Running).
  • \n", - "
  • Date Range Picker: Select a start and end date to narrow down jobs based on creation date.
  • \n", - "
  • Properties Filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.
  • \n", + "
  • Quick search field: Use the search bar above the table to find jobs by any visible field.
  • \n", + "
  • Column-specific filters: Filter data directly in each column using their respective filter options.
  • \n", + "
  • Job state dropdown: Filter jobs by their state (e.g., Finished, Running).
  • \n", + "
  • Date range picker: Select a start and end date to narrow down jobs based on creation date.
  • \n", + "
  • Properties filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.
  • \n", "
\n", - "

Table Actions

\n", + "

Table actions

\n", "
    \n", - "
  • Editable Fields: Edit the label and description of a job directly by clicking on the respective fields.
  • \n", + "
  • Editable fields: Edit the label and description of a job directly by clicking on the respective fields.
  • \n", "
  • Inspect: Click on a job's ID link to view detailed information.
  • \n", "
  • Delete: Remove a job by clicking the \"Delete\" link in its row. A confirmation page will be opened.
  • \n", "
  • Download: Download raw data (i.e. input and output files) and/or the AiiDA archive of a job using the \"Download\" link.
  • \n", "
\n", - "

Display Options

\n", + "

Display options

\n", "
    \n", "
  • Sorting: Click any column header to sort the table by that column.
  • \n", - "
  • Column Management: Show, hide columns to customize the table view.
  • \n", - "
  • Time Format: Toggle between \"Absolute\" (specific dates) and \"Relative\" (time elapsed).
  • \n", - "
  • ID Format: Switch between \"PK\" or \"UUID\" for job identification.
  • \n", + "
  • Column management: Show, hide columns to customize the table view.
  • \n", + "
  • Time format: Toggle between \"Absolute\" (specific dates) and \"Relative\" (time elapsed).
  • \n", + "
  • ID format: Switch between \"PK\" or \"UUID\" for job identification.
  • \n", "
\n", "
\n", " \"\"\"\n", diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index bab78bb91..f8446244d 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -15,13 +15,13 @@ COLUMNS = { "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, "Creation time absolute": { - "headerName": "Creation Time ⏰ (absolute)", + "headerName": "Creation time ⏰ (absolute)", "type": "date", "width": 150, "editable": False, }, "Creation time relative": { - "headerName": "Creation Time ⏰ (relative)", + "headerName": "Creation time ⏰ (relative)", "width": 150, "editable": False, }, @@ -34,7 +34,7 @@ "editable": True, "hide": True, }, - "Relax_type": {"headerName": "Relax_type", "editable": False, "hide": True}, + "Relax_type": {"headerName": "Relax type", "editable": False, "hide": True}, "Delete": {"headerName": "Delete", "dataType": "link", "editable": False}, "Download": {"headerName": "Download", "dataType": "link", "editable": False}, "UUID": {"headerName": "UUID", "editable": False, "hide": True}, @@ -209,7 +209,7 @@ def setup_widgets(self): children=[ ipw.HTML("

Display options:

"), ipw.VBox( - [ + children=[ self.toggle_time_format, self.toggle_id_format, # self.toggle_multi_selection, @@ -218,24 +218,23 @@ def setup_widgets(self): ], layout=ipw.Layout( border="1px solid #ddd", - padding="0px", - margin="0px, 0px, 100px, 0px", - background_color="#f9f9f9", - border_radius="5px", - box_shadow="0 2px 5px rgba(0, 0, 0, 0.1)", ), ) filters = ipw.VBox( children=[ ipw.HTML("

Filters:

"), ipw.VBox( - [ + children=[ self.job_state_dropdown, self.time_box, ipw.VBox( - [self.properties_filter_description, self.properties_box], + children=[ + self.properties_filter_description, + self.properties_box, + ], layout=ipw.Layout( - border="1px solid #ddd", padding="5px", margin="5px" + border="1px solid #ddd", # fmt: off + margin="5px", ), ), ] @@ -243,11 +242,6 @@ def setup_widgets(self): ], layout=ipw.Layout( border="1px solid #ddd", - padding="0px", - margin="0px, 0px, 100px, 0px", - background_color="#f9f9f9", - border_radius="5px", - box_shadow="0 2px 5px rgba(0, 0, 0, 0.1)", ), ) From eac7275651949b3bb6b6cba72a5ecce59cd8d13a Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 15:36:01 +0000 Subject: [PATCH 05/12] Add exit status and message --- src/aiidalab_qe/app/utils/search_jobs.py | 35 ++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index f8446244d..8606d4a26 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -12,21 +12,37 @@ "killed": "❌", } + +def determine_state_icon(row): + state = row["State"].lower() + if state == "finished" and row["Exit_status"] != 0: + return f"Finished{STATE_ICONS['excepted']}" + return f"{state.capitalize()}{STATE_ICONS.get(state, '')}" + + COLUMNS = { "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, "Creation time absolute": { "headerName": "Creation time ⏰ (absolute)", "type": "date", - "width": 150, + "width": 100, "editable": False, }, "Creation time relative": { "headerName": "Creation time ⏰ (relative)", - "width": 150, + "width": 100, "editable": False, }, "Structure": {"headerName": "Structure", "editable": False}, "State": {"headerName": "State 🟢", "editable": False}, + "Status": {"headerName": "Status", "editable": False, "hide": True}, + "Exit_status": { + "headerName": "Exit status", + "type": "number", + "editable": False, + "hide": True, + }, + "Exit_message": {"headerName": "Exit message", "editable": False}, "Label": {"headerName": "Label", "width": 300, "editable": True}, "Description": { "headerName": "Description", @@ -67,6 +83,9 @@ def load_data(self): "extras.structure", "ctime", "attributes.process_state", + "attributes.process_status", + "attributes.exit_status", + "attributes.exit_message", "label", "description", "extras.workchain.relax_type", @@ -78,6 +97,9 @@ def load_data(self): "Structure", "ctime", "State", + "Status", + "Exit_status", + "Exit_message", "Label", "Description", "Relax_type", @@ -122,6 +144,7 @@ def load_data(self): # Initialize empty columns for an empty DataFrame df["Creation time"] = pd.Series(dtype="str") df["Delete"] = pd.Series(dtype="str") + print(df[0:5]) return df[ [ "PK_with_link", @@ -130,6 +153,9 @@ def load_data(self): "Creation time relative", "Structure", "State", + "Status", + "Exit_status", + "Exit_message", "Label", "Description", "Relax_type", @@ -271,9 +297,8 @@ def update_table_value(self, display_df): ) # - display_df["State"] = display_df["State"].apply( - lambda x: f"{x.capitalize()}{STATE_ICONS.get(x.lower())}" - ) + if not display_df.empty: + display_df["State"] = display_df.apply(determine_state_icon, axis=1) display_df = display_df.drop(columns=["Properties", "ctime"]) columns = [] for col in display_df.columns: From 115a40392e913deb14b4a7d95ad8ab75dbdfc8c3 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 17:14:54 +0100 Subject: [PATCH 06/12] Add test --- setup.cfg | 2 ++ tests/test_calculation_history.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/test_calculation_history.py diff --git a/setup.cfg b/setup.cfg index c594b09fd..d7716144c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,8 @@ install_requires = aiida-wannier90-workflows==2.3.0 pymatgen==2024.5.1 anywidget==0.9.13 + table_widget~=0.0.2 + python_requires = >=3.9 [options.packages.find] diff --git a/tests/test_calculation_history.py b/tests/test_calculation_history.py new file mode 100644 index 000000000..4da578921 --- /dev/null +++ b/tests/test_calculation_history.py @@ -0,0 +1,12 @@ +import pytest + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_calculation_history(generate_qeapp_workchain): + from aiidalab_qe.app.utils.search_jobs import CalculationHistory + + workchain = generate_qeapp_workchain() + workchain.node.seal() + + calculation_history = CalculationHistory() + calculation_history.load_table() + assert len(calculation_history.table.data) == 1 \ No newline at end of file From 7da19e2e4c5f03d917fe3a4d3cf156cc9473f979 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:15:06 +0000 Subject: [PATCH 07/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_calculation_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_calculation_history.py b/tests/test_calculation_history.py index 4da578921..345a20f3b 100644 --- a/tests/test_calculation_history.py +++ b/tests/test_calculation_history.py @@ -1,5 +1,6 @@ import pytest + @pytest.mark.usefixtures("aiida_profile_clean") def test_calculation_history(generate_qeapp_workchain): from aiidalab_qe.app.utils.search_jobs import CalculationHistory @@ -9,4 +10,4 @@ def test_calculation_history(generate_qeapp_workchain): calculation_history = CalculationHistory() calculation_history.load_table() - assert len(calculation_history.table.data) == 1 \ No newline at end of file + assert len(calculation_history.table.data) == 1 From 58c735f474bc96f153f7b364e59339fe57656a73 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 16:27:34 +0000 Subject: [PATCH 08/12] update test --- tests/test_calculation_history.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_calculation_history.py b/tests/test_calculation_history.py index 345a20f3b..10a7c8196 100644 --- a/tests/test_calculation_history.py +++ b/tests/test_calculation_history.py @@ -1,8 +1,4 @@ -import pytest - - -@pytest.mark.usefixtures("aiida_profile_clean") -def test_calculation_history(generate_qeapp_workchain): +def test_calculation_history(sssp, generate_qeapp_workchain): from aiidalab_qe.app.utils.search_jobs import CalculationHistory workchain = generate_qeapp_workchain() @@ -10,4 +6,4 @@ def test_calculation_history(generate_qeapp_workchain): calculation_history = CalculationHistory() calculation_history.load_table() - assert len(calculation_history.table.data) == 1 + assert len(calculation_history.table.data) >= 1 From ed9bdf25d817a7853ba4fff0d58a983281ccbd6e Mon Sep 17 00:00:00 2001 From: superstar54 Date: Thu, 9 Jan 2025 17:16:20 +0000 Subject: [PATCH 09/12] Add control for the page guid --- calculation_history.ipynb | 148 +++++++++--------- .../templates/calculation_history_guide.jinja | 28 ++++ src/aiidalab_qe/app/utils/search_jobs.py | 1 - 3 files changed, 98 insertions(+), 79 deletions(-) create mode 100644 src/aiidalab_qe/app/static/templates/calculation_history_guide.jinja diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 88d1525bd..5463b9084 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -6,83 +6,11 @@ "metadata": {}, "outputs": [], "source": [ - "import ipywidgets as ipw\n", - "\n", - "how_to = ipw.HTML(\n", - " \"\"\"\n", - " \n", - "
Calculation history
\n", - "
\n", - "

How to use this page

\n", - "

\n", - " This page allows you to view, filter, sort, and manage your calculation history.\n", - "

\n", - "

Filters and search

\n", - "
    \n", - "
  • Quick search field: Use the search bar above the table to find jobs by any visible field.
  • \n", - "
  • Column-specific filters: Filter data directly in each column using their respective filter options.
  • \n", - "
  • Job state dropdown: Filter jobs by their state (e.g., Finished, Running).
  • \n", - "
  • Date range picker: Select a start and end date to narrow down jobs based on creation date.
  • \n", - "
  • Properties filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.
  • \n", - "
\n", - "

Table actions

\n", - "
    \n", - "
  • Editable fields: Edit the label and description of a job directly by clicking on the respective fields.
  • \n", - "
  • Inspect: Click on a job's ID link to view detailed information.
  • \n", - "
  • Delete: Remove a job by clicking the \"Delete\" link in its row. A confirmation page will be opened.
  • \n", - "
  • Download: Download raw data (i.e. input and output files) and/or the AiiDA archive of a job using the \"Download\" link.
  • \n", - "
\n", - "

Display options

\n", - "
    \n", - "
  • Sorting: Click any column header to sort the table by that column.
  • \n", - "
  • Column management: Show, hide columns to customize the table view.
  • \n", - "
  • Time format: Toggle between \"Absolute\" (specific dates) and \"Relative\" (time elapsed).
  • \n", - "
  • ID format: Switch between \"PK\" or \"UUID\" for job identification.
  • \n", - "
\n", - "
\n", - " \"\"\"\n", - ")\n", - "\n", - "how_to" + "%%javascript\n", + "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", + " return false;\n", + "}\n", + "document.title='AiiDAlab QE app calculation history'" ] }, { @@ -108,6 +36,70 @@ "load_css(css_path=\"src/aiidalab_qe/app/static/styles\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as ipw\n", + "from importlib_resources import files\n", + "from IPython.display import Image\n", + "from jinja2 import Environment\n", + "\n", + "from aiidalab_qe.app.static import templates\n", + "from aiidalab_qe.common.infobox import InfoBox\n", + "\n", + "output = ipw.Output()\n", + "logo_img = Image(\n", + " filename=\"docs/source/_static/logo.png\",\n", + " width=\"700\",\n", + ")\n", + "logo = ipw.Output()\n", + "with logo:\n", + " display(logo_img)\n", + "logo.add_class(\"logo\")\n", + "subtitle = ipw.HTML(\"

🎉 Calculation history 🎉

\")\n", + "env = Environment()\n", + "guide_template = (\n", + " files(templates).joinpath(\"calculation_history_guide.jinja\").read_text()\n", + ")\n", + "guide = ipw.HTML(env.from_string(guide_template).render())\n", + "\n", + "\n", + "info_container = InfoBox(layout=ipw.Layout(margin=\"14px 2px 0\"))\n", + "guide_toggle = ipw.ToggleButton(\n", + " layout=ipw.Layout(width=\"150px\"),\n", + " button_style=\"\",\n", + " icon=\"book\",\n", + " value=False,\n", + " description=\"Page guide\",\n", + " tooltip=\"Learn how to use the app\",\n", + " disabled=False,\n", + ")\n", + "\n", + "\n", + "def _on_guide_toggle(change: dict):\n", + " \"\"\"Toggle the guide section.\"\"\"\n", + " if change[\"new\"]:\n", + " info_container.children = [\n", + " guide,\n", + " ]\n", + " info_container.layout.display = \"flex\"\n", + " else:\n", + " info_container.children = []\n", + " info_container.layout.display = \"none\"\n", + "\n", + "\n", + "guide_toggle.observe(\n", + " _on_guide_toggle,\n", + " \"value\",\n", + ")\n", + "\n", + "controls = ipw.VBox(children=[logo, subtitle, guide_toggle, info_container])\n", + "controls" + ] + }, { "cell_type": "code", "execution_count": null, @@ -132,7 +124,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "base", "language": "python", "name": "python3" }, diff --git a/src/aiidalab_qe/app/static/templates/calculation_history_guide.jinja b/src/aiidalab_qe/app/static/templates/calculation_history_guide.jinja new file mode 100644 index 000000000..b09475734 --- /dev/null +++ b/src/aiidalab_qe/app/static/templates/calculation_history_guide.jinja @@ -0,0 +1,28 @@ +
+

How to use this page

+

+ This page allows you to view, filter, sort, and manage your calculation history. +

+

Filters and search

+
    +
  • Quick search field: Use the search bar above the table to find jobs by any visible field.
  • +
  • Column-specific filters: Filter data directly in each column using their respective filter options.
  • +
  • Job state dropdown: Filter jobs by their state (e.g., Finished, Running).
  • +
  • Date range picker: Select a start and end date to narrow down jobs based on creation date.
  • +
  • Properties filter: Select one or more properties to narrow the results. Only calculations that include all the selected properties will be displayed. Leave all checkboxes unselected to include calculations regardless of their properties.
  • +
+

Table actions

+
    +
  • Editable fields: Edit the label and description of a job directly by clicking on the respective fields.
  • +
  • Inspect: Click on a job's ID link to view detailed information.
  • +
  • Delete: Remove a job by clicking the "Delete" link in its row. A confirmation page will be opened.
  • +
  • Download: Download raw data (i.e. input and output files) and/or the AiiDA archive of a job using the "Download" link.
  • +
+

Display options

+
    +
  • Sorting: Click any column header to sort the table by that column.
  • +
  • Column management: Show, hide columns to customize the table view.
  • +
  • Time format: Toggle between "Absolute" (specific dates) and "Relative" (time elapsed).
  • +
  • ID format: Switch between "PK" or "UUID" for job identification.
  • +
+
diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 8606d4a26..94681f758 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -144,7 +144,6 @@ def load_data(self): # Initialize empty columns for an empty DataFrame df["Creation time"] = pd.Series(dtype="str") df["Delete"] = pd.Series(dtype="str") - print(df[0:5]) return df[ [ "PK_with_link", From 30b7953dc2a0aedf85d33b962a7912906e70be52 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Fri, 10 Jan 2025 06:35:39 +0000 Subject: [PATCH 10/12] Remove logo and use only one border --- calculation_history.ipynb | 14 ++------------ src/aiidalab_qe/app/utils/search_jobs.py | 8 -------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 5463b9084..8d66a1dde 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -44,22 +44,12 @@ "source": [ "import ipywidgets as ipw\n", "from importlib_resources import files\n", - "from IPython.display import Image\n", "from jinja2 import Environment\n", "\n", "from aiidalab_qe.app.static import templates\n", "from aiidalab_qe.common.infobox import InfoBox\n", "\n", - "output = ipw.Output()\n", - "logo_img = Image(\n", - " filename=\"docs/source/_static/logo.png\",\n", - " width=\"700\",\n", - ")\n", - "logo = ipw.Output()\n", - "with logo:\n", - " display(logo_img)\n", - "logo.add_class(\"logo\")\n", - "subtitle = ipw.HTML(\"

🎉 Calculation history 🎉

\")\n", + "title = ipw.HTML(\"

🎉 Calculation history 🎉

\")\n", "env = Environment()\n", "guide_template = (\n", " files(templates).joinpath(\"calculation_history_guide.jinja\").read_text()\n", @@ -96,7 +86,7 @@ " \"value\",\n", ")\n", "\n", - "controls = ipw.VBox(children=[logo, subtitle, guide_toggle, info_container])\n", + "controls = ipw.VBox(children=[title, guide_toggle, info_container])\n", "controls" ] }, diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 94681f758..4c1bc92bc 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -240,13 +240,6 @@ def setup_widgets(self): # self.toggle_multi_selection, ] ), - ], - layout=ipw.Layout( - border="1px solid #ddd", - ), - ) - filters = ipw.VBox( - children=[ ipw.HTML("

Filters:

"), ipw.VBox( children=[ @@ -272,7 +265,6 @@ def setup_widgets(self): self.main.children = [ display_options, - filters, self.table, ] self.update_table_value(self.df) From a1dc8ce09172b72ad40436785c23f6278388e769 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Fri, 10 Jan 2025 07:41:26 +0000 Subject: [PATCH 11/12] Adjust style as suggestion from review --- calculation_history.ipynb | 4 +++- src/aiidalab_qe/app/utils/search_jobs.py | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/calculation_history.ipynb b/calculation_history.ipynb index 8d66a1dde..11a1130ff 100644 --- a/calculation_history.ipynb +++ b/calculation_history.ipynb @@ -49,7 +49,9 @@ "from aiidalab_qe.app.static import templates\n", "from aiidalab_qe.common.infobox import InfoBox\n", "\n", - "title = ipw.HTML(\"

🎉 Calculation history 🎉

\")\n", + "title = ipw.HTML(\n", + " \"

Calculation history

\", layout=ipw.Layout(margin=\"0 0 15px 0\")\n", + ")\n", "env = Environment()\n", "guide_template = (\n", " files(templates).joinpath(\"calculation_history_guide.jinja\").read_text()\n", diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 4c1bc92bc..9ef4df650 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -232,7 +232,6 @@ def setup_widgets(self): self.job_state_dropdown.observe(self.apply_filters, names="value") display_options = ipw.VBox( children=[ - ipw.HTML("

Display options:

"), ipw.VBox( children=[ self.toggle_time_format, @@ -240,7 +239,14 @@ def setup_widgets(self): # self.toggle_multi_selection, ] ), - ipw.HTML("

Filters:

"), + ], + layout=ipw.Layout( + border="1px solid lightgray", + padding="0.5em", + ), + ) + filters = ipw.VBox( + children=[ ipw.VBox( children=[ self.job_state_dropdown, @@ -251,7 +257,8 @@ def setup_widgets(self): self.properties_box, ], layout=ipw.Layout( - border="1px solid #ddd", # fmt: off + border="1px solid lightgray", # fmt: off + padding="0.5em", margin="5px", ), ), @@ -259,12 +266,16 @@ def setup_widgets(self): ), ], layout=ipw.Layout( - border="1px solid #ddd", + border="1px solid lightgray", + padding="0.5em", ), ) self.main.children = [ + ipw.HTML("

Display options:

"), display_options, + ipw.HTML("

Filters:

"), + filters, self.table, ] self.update_table_value(self.df) From fcd8467948edc2a4349a95448f18cbcad2eb28a5 Mon Sep 17 00:00:00 2001 From: superstar54 Date: Fri, 10 Jan 2025 09:53:20 +0000 Subject: [PATCH 12/12] Drop pandas and handling empty data --- src/aiidalab_qe/app/utils/search_jobs.py | 372 ++++++++++++----------- 1 file changed, 202 insertions(+), 170 deletions(-) diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/search_jobs.py index 9ef4df650..851c6994d 100644 --- a/src/aiidalab_qe/app/utils/search_jobs.py +++ b/src/aiidalab_qe/app/utils/search_jobs.py @@ -1,5 +1,6 @@ +from datetime import datetime, timezone + import ipywidgets as ipw -import pandas as pd from table_widget import TableWidget from aiida.orm import QueryBuilder, load_node @@ -14,46 +15,48 @@ def determine_state_icon(row): - state = row["State"].lower() - if state == "finished" and row["Exit_status"] != 0: + """Attach an icon to the displayed job state.""" + state = row["state"].lower() + if state == "finished" and row.get("exit_status", 0) != 0: return f"Finished{STATE_ICONS['excepted']}" return f"{state.capitalize()}{STATE_ICONS.get(state, '')}" COLUMNS = { - "ID": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, - "Creation time absolute": { + "id": {"headerName": "ID 🔗", "dataType": "link", "editable": False}, + "creation_time_absolute": { "headerName": "Creation time ⏰ (absolute)", "type": "date", "width": 100, "editable": False, }, - "Creation time relative": { + "creation_time_relative": { "headerName": "Creation time ⏰ (relative)", "width": 100, "editable": False, }, - "Structure": {"headerName": "Structure", "editable": False}, - "State": {"headerName": "State 🟢", "editable": False}, - "Status": {"headerName": "Status", "editable": False, "hide": True}, - "Exit_status": { + "structure": {"headerName": "Structure", "editable": False}, + "state": {"headerName": "State 🟢", "editable": False}, + "status": {"headerName": "Status", "editable": False, "hide": True}, + "exit_status": { "headerName": "Exit status", "type": "number", "editable": False, "hide": True, }, - "Exit_message": {"headerName": "Exit message", "editable": False}, - "Label": {"headerName": "Label", "width": 300, "editable": True}, - "Description": { + "exit_message": {"headerName": "Exit message", "editable": False}, + "label": {"headerName": "Label", "width": 300, "editable": True}, + "description": { "headerName": "Description", "width": 300, "editable": True, "hide": True, }, - "Relax_type": {"headerName": "Relax type", "editable": False, "hide": True}, - "Delete": {"headerName": "Delete", "dataType": "link", "editable": False}, - "Download": {"headerName": "Download", "dataType": "link", "editable": False}, - "UUID": {"headerName": "UUID", "editable": False, "hide": True}, + "relax_type": {"headerName": "Relax type", "editable": False, "hide": True}, + "delete": {"headerName": "Delete", "dataType": "link", "editable": False}, + "download": {"headerName": "Download", "dataType": "link", "editable": False}, + "uuid": {"headerName": "UUID", "editable": False, "hide": True}, + "properties": {"headerName": "Properties", "editable": False, "hide": True}, } @@ -64,17 +67,29 @@ def __init__(self): self.table = TableWidget(style={"margin-top": "20px"}) def on_row_update(change): - node = load_node(change["new"]["UUID"]) - node.label = change["new"]["Label"] - node.description = change["new"]["Description"] + # When the user updates 'label' or 'description' in the table, + # reflect these changes in the corresponding AiiDA node. + node = load_node(change["new"]["uuid"]) + node.label = change["new"]["label"] + node.description = change["new"]["description"] self.table.observe(on_row_update, "updatedRow") + # This will hold the raw data (list of dicts) from the database + self.data = [] + # This will hold the currently displayed data (filtered, with toggles applied, etc.) + self.display_data = [] + def load_table(self): - self.df = self.load_data() + """Populate the table after initialization.""" + self.data = self.load_data() self.setup_widgets() def load_data(self): + """Fetch the QeAppWorkChain results using the QueryBuilder and + return a list of dictionaries with all required columns. + + """ from aiidalab_qe.workflows import QeAppWorkChain projections = [ @@ -91,102 +106,107 @@ def load_data(self): "extras.workchain.relax_type", "extras.workchain.properties", ] - headers = [ - "PK", - "UUID", - "Structure", - "ctime", - "State", - "Status", - "Exit_status", - "Exit_message", - "Label", - "Description", - "Relax_type", - "Properties", - ] qb = QueryBuilder() qb.append(QeAppWorkChain, project=projections, tag="process") qb.order_by({"process": {"ctime": "desc"}}) results = qb.all() - df = pd.DataFrame(results, columns=headers) - # Check if DataFrame is not empty - if not df.empty: - df["Creation time absolute"] = df["ctime"].apply( - lambda x: x.strftime("%Y-%m-%d %H:%M:%S") - ) - now = pd.Timestamp.now(tz="UTC") - df["Creation time relative"] = df["ctime"].apply( - lambda x: f"{(now - x).days}D ago" if pd.notnull(x) else "N/A" - ) - df["Delete"] = df["PK"].apply( - lambda pk: f'Delete' - ) - df["Download"] = df["PK"].apply( - lambda pk: f'Download' + data = [] + if not results: + return data + + now = datetime.now(timezone.utc) + + for row in results: + ( + pk, + uuid, + structure, + creation_time, + state, + status, + exit_status, + exit_message, + label, + description, + relax_type, + properties, + ) = row + + creation_time_str = ( + creation_time.strftime("%Y-%m-%d %H:%M:%S") if creation_time else "" ) - # add a link to the pk so that the user can inspect the calculation - df["PK_with_link"] = df["PK"].apply( - lambda pk: f'{pk}' + if creation_time: + days_ago = (now - creation_time).days + creation_time_rel = f"{days_ago}D ago" + else: + creation_time_rel = "N/A" + + # Transform "waiting" to "running" for readbility + if state == "waiting": + state = "running" + + # Prepare link-based values + pk_with_link = f'{pk}' + uuid_with_link = ( + f'{uuid[:8]}' ) - # Store initial part of the UUID - df["UUID_with_link"] = df.apply( - lambda row: f'{row["UUID"][:8]}', - axis=1, + delete_link = f'Delete' + download_link = ( + f'Download' ) - # replace all "waiting" states with "running" - df["State"] = df["State"].apply( - lambda x: "running" if x == "waiting" else x + + # Make sure properties is a list (avoid None) + properties = properties if properties is not None else [] + + data.append( + { + "pk_with_link": pk_with_link, + "uuid_with_link": uuid_with_link, + "creation_time_absolute": creation_time_str, + "creation_time_relative": creation_time_rel, + "structure": structure, + "state": state, + "status": status, + "exit_status": exit_status, + "exit_message": exit_message, + "label": label, + "description": description, + "relax_type": relax_type, + "uuid": uuid, + "delete": delete_link, + "download": download_link, + "properties": properties, + "creation_time": creation_time, + } ) - else: - # Initialize empty columns for an empty DataFrame - df["Creation time"] = pd.Series(dtype="str") - df["Delete"] = pd.Series(dtype="str") - return df[ - [ - "PK_with_link", - "UUID_with_link", - "Creation time absolute", - "Creation time relative", - "Structure", - "State", - "Status", - "Exit_status", - "Exit_message", - "Label", - "Description", - "Relax_type", - "UUID", - "Delete", - "Download", - "Properties", - "ctime", - ] - ] + + return data def setup_widgets(self): - unique_properties = set(self.df["Properties"].explode().dropna()) - unique_properties.discard(None) + """Create widgets for filtering, toggles for display, etc.""" + # Gather unique properties + all_properties = set() + for row in self.data: + for prop in row["properties"]: + if prop is not None: + all_properties.add(prop) + + # Build a set of checkboxes for properties property_checkboxes = [ ipw.Checkbox( value=False, description=prop, - Layout=ipw.Layout(description_width="initial"), indent=False, + layout=ipw.Layout(description_width="initial"), ) - for prop in unique_properties + for prop in sorted(all_properties) ] - self.properties_box = ipw.HBox( - children=property_checkboxes, - indent=True, - ) + + self.properties_box = ipw.HBox(property_checkboxes) self.properties_filter_description = ipw.HTML("Filter by properties:") - # Replace 'None' in 'Properties' with an empty list - self.df["Properties"] = self.df["Properties"].apply( - lambda x: [] if x is None else x - ) + self.job_state_dropdown = ipw.Dropdown( options={ "Any": "", @@ -198,53 +218,43 @@ def setup_widgets(self): value="", # Default value corresponding to "Any" description="Job state:", ) + self.toggle_time_format = ipw.ToggleButtons( options=["Absolute", "Relative"], - value="Absolute", # Default to Absolute time + value="Absolute", # Default to showing Absolute time description="Time format:", ) self.toggle_time_format.observe(self.update_table_visibility, names="value") + self.toggle_id_format = ipw.ToggleButtons( - options=["PK", "UUID"], - value="PK", # Default to PK + options=["pk", "uuid"], + value="pk", description="ID format:", ) self.toggle_id_format.observe(self.update_table_visibility, names="value") - # self.toggle_multi_selection = ipw.ToggleButtons( - # options=["On", "Off"], - # value="Off", - # description="Checkbox selection", - # tooltips=["Enable multiple selection.", "Disable multiple selection"], - # ) - # self.toggle_multi_selection.observe( - # self.update_table_configuration, names="value" - # ) + # Date pickers for range-based filtering self.time_start = ipw.DatePicker(description="Start time:") self.time_end = ipw.DatePicker(description="End time:") self.time_box = ipw.HBox([self.time_start, self.time_end]) - # self.apply_filters_btn = ipw.Button(description='Apply Filters') - # self.apply_filters_btn.on_click(self.apply_filters) + + # Connect checkboxes and dropdowns to the filter logic for cb in property_checkboxes: cb.observe(self.apply_filters, names="value") self.time_start.observe(self.apply_filters, names="value") self.time_end.observe(self.apply_filters, names="value") self.job_state_dropdown.observe(self.apply_filters, names="value") + display_options = ipw.VBox( children=[ - ipw.VBox( - children=[ - self.toggle_time_format, - self.toggle_id_format, - # self.toggle_multi_selection, - ] - ), + ipw.VBox(children=[self.toggle_time_format, self.toggle_id_format]), ], layout=ipw.Layout( border="1px solid lightgray", padding="0.5em", ), ) + filters = ipw.VBox( children=[ ipw.VBox( @@ -257,7 +267,7 @@ def setup_widgets(self): self.properties_box, ], layout=ipw.Layout( - border="1px solid lightgray", # fmt: off + border="1px solid lightgray", padding="0.5em", margin="5px", ), @@ -278,69 +288,91 @@ def setup_widgets(self): filters, self.table, ] - self.update_table_value(self.df) - def update_table_value(self, display_df): - # Adjust the Creation time column based on the toggle state - COLUMNS["Creation time relative"]["hide"] = ( + self.update_table_value(self.data) + + def update_table_value(self, data_list): + """Prepare the data to be shown (adding or hiding columns, etc.), + and load it into `self.table`. + """ + # Adjust which creation_time columns to hide + COLUMNS["creation_time_relative"]["hide"] = ( self.toggle_time_format.value != "Relative" ) - COLUMNS["Creation time absolute"]["hide"] = ( + COLUMNS["creation_time_absolute"]["hide"] = ( self.toggle_time_format.value != "Absolute" ) - # Adjust the ID column based on the toggle state - if self.toggle_id_format.value == "PK": - display_df = display_df.rename(columns={"PK_with_link": "ID"}).drop( - columns=["UUID_with_link"] - ) - else: - display_df = display_df.rename(columns={"UUID_with_link": "ID"}).drop( - columns=["PK_with_link"] - ) - # - if not display_df.empty: - display_df["State"] = display_df.apply(determine_state_icon, axis=1) - display_df = display_df.drop(columns=["Properties", "ctime"]) + # Build a new list that has an 'id' column, etc. + display_data = [] + for row in data_list: + row_copy = dict(row) + # Switch the 'id' column depending on pk vs uuid toggle + if self.toggle_id_format.value == "pk": + row_copy["id"] = row_copy["pk_with_link"] + else: + row_copy["id"] = row_copy["uuid_with_link"] + + # Overwrite "state" with icon-based representation + row_copy["state"] = determine_state_icon(row_copy) + display_data.append(row_copy) + + # Figure out which columns to show the table columns = [] - for col in display_df.columns: - column = COLUMNS.get(col) - column["field"] = col - columns.append(COLUMNS[col]) - self.table.from_data(display_df, columns=columns) + for key in COLUMNS.keys(): + col_spec = dict(COLUMNS[key]) + col_spec["field"] = key + columns.append(col_spec) + + self.table.from_data(display_data, columns=columns) def apply_filters(self, _): + """Filter the raw data based on job state, selected properties, + and date range. Then update the table display. + """ + filtered = [] + selected_properties = [ cb.description for cb in self.properties_box.children if cb.value ] - filtered_df = self.df.copy() - filtered_df = filtered_df[ - filtered_df["State"].str.contains(self.job_state_dropdown.value) - ] - if selected_properties: - filtered_df = filtered_df[ - filtered_df["Properties"].apply( - lambda x: all(item in x for item in selected_properties) - ) - ] - if self.time_start.value and self.time_end.value: - start_time = pd.to_datetime(self.time_start.value).normalize() - end_time = pd.to_datetime(self.time_end.value).normalize() + pd.Timedelta( - days=1, milliseconds=-1 - ) - start_time = start_time.tz_localize("UTC") - end_time = end_time.tz_localize("UTC") - filtered_df = filtered_df[ - (filtered_df["ctime"] >= start_time) - & (filtered_df["ctime"] <= end_time) - ] - self.update_table_value(filtered_df) + + # Convert DatePicker values (which are dates) into datetimes with UTC + start_time = None + end_time = None + if self.time_start.value is not None: + start_time = datetime.combine(self.time_start.value, datetime.min.time()) + start_time = start_time.replace(tzinfo=timezone.utc) + + if self.time_end.value is not None: + end_time = datetime.combine(self.time_end.value, datetime.max.time()) + end_time = end_time.replace(tzinfo=timezone.utc) + + # State filter (empty string means "Any") + desired_state_substring = self.job_state_dropdown.value + + for row in self.data: + if desired_state_substring: + if desired_state_substring not in row["state"].lower(): + continue + + row_props = row["properties"] + if selected_properties: + # Must have all selected properties in row_props + if not all(prop in row_props for prop in selected_properties): + continue + + ctime = row.get("creation_time", None) + if ctime is not None: + if start_time and ctime < start_time: + continue + if end_time and ctime > end_time: + continue + + filtered.append(row) + + self.update_table_value(filtered) def update_table_visibility(self, _): - # Reapply filters to refresh the table visibility when the checkbox changes + """Called when toggles for time format or ID format change.""" + # simply re-apply filters (which triggers a re-draw). self.apply_filters(None) - - def update_table_configuration(self, _): - enable = True if self.toggle_multi_selection.value == "On" else False - config = {"pagination": True, "checkboxSelection": enable} - self.table.config = config