diff --git a/app.ipynb b/app.ipynb new file mode 100644 index 000000000..a1dd5e87b --- /dev/null +++ b/app.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiida import load_profile\n", + "\n", + "load_profile();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiidalab_qe.solara.main import Page\n", + "\n", + "Page()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index 616c5ff9b..44175c45e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ output-format = "full" target-version = "py39" [tool.ruff.lint] -ignore = ["E501", "E402", "TRY003", "RUF012", "N806"] +ignore = ["E501", "E402", "TRY003", "RUF012", "N806", "N802", "N816"] select = [ "A", # flake8-builtins "ARG", # flake8-unused-arguments @@ -42,7 +42,7 @@ select = [ "PLC", # pylint convention rules "RUF", # ruff-specific rules "TRY", # Tryceratops - "UP" # pyupgrade + "UP" # pyupgrade ] [tool.ruff.lint.isort] diff --git a/src/aiidalab_qe/solara/__init__.py b/src/aiidalab_qe/solara/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/aiidalab_qe/solara/assets/styles/css/main.css b/src/aiidalab_qe/solara/assets/styles/css/main.css new file mode 100644 index 000000000..981df8c33 --- /dev/null +++ b/src/aiidalab_qe/solara/assets/styles/css/main.css @@ -0,0 +1,9 @@ +@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"); + +#site { + overflow-y: scroll !important; +} + +.output_subarea { + max-width: unset !important; +} diff --git a/src/aiidalab_qe/solara/common/__init__.py b/src/aiidalab_qe/solara/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/aiidalab_qe/solara/common/state.py b/src/aiidalab_qe/solara/common/state.py new file mode 100644 index 000000000..318fc04c7 --- /dev/null +++ b/src/aiidalab_qe/solara/common/state.py @@ -0,0 +1,29 @@ +from enum import Enum + + +class State(Enum): + FAIL = -1 + INIT = 0 + CONFIGURED = 1 + READY = 2 + ACTIVE = 3 + SUCCESS = 4 + + +STATE_ICONS = { + State.INIT: "\u25cb", + State.READY: "\u25ce", + State.CONFIGURED: "\u25cf", + State.ACTIVE: "\u231b", + State.SUCCESS: "\u2713", + State.FAIL: "\u00d7", +} + +BG_COLORS = { + State.INIT: "#eee", + State.READY: "#fcf8e3", + State.CONFIGURED: "#fcf8e3", + State.ACTIVE: "#d9edf7", + State.SUCCESS: "#dff0d8", + State.FAIL: "#f8d7da", +} diff --git a/src/aiidalab_qe/solara/components/__init__.py b/src/aiidalab_qe/solara/components/__init__.py new file mode 100644 index 000000000..be78dacf5 --- /dev/null +++ b/src/aiidalab_qe/solara/components/__init__.py @@ -0,0 +1,15 @@ +from .app import WizardApp +from .parameters import ParametersStep +from .resources import ResourcesStep +from .results import ResultsStep +from .structure import StructureStep +from .submission import SubmissionStep + +__all__ = [ + "ParametersStep", + "ResourcesStep", + "ResultsStep", + "StructureStep", + "SubmissionStep", + "WizardApp", +] diff --git a/src/aiidalab_qe/solara/components/app/__init__.py b/src/aiidalab_qe/solara/components/app/__init__.py new file mode 100644 index 000000000..7f78fa465 --- /dev/null +++ b/src/aiidalab_qe/solara/components/app/__init__.py @@ -0,0 +1,6 @@ +from .app import App, WizardApp + +__all__ = [ + "App", + "WizardApp", +] diff --git a/src/aiidalab_qe/solara/components/app/app.py b/src/aiidalab_qe/solara/components/app/app.py new file mode 100644 index 000000000..5bcb46409 --- /dev/null +++ b/src/aiidalab_qe/solara/components/app/app.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import solara +from solara.alias import rv + +from ..header import Header, LogoProps +from ..navbar import NavBar, NavItemProps +from ..wizard import StepProps, Wizard + + +@solara.component +def App( + title: str, + subtitle: str = "", + logo: LogoProps | None = None, + nav_items: list[NavItemProps] | None = None, + children: list[solara.Element] | None = None, +): + with rv.Container(class_="text-center"): + Header(title, subtitle, logo) + if nav_items: + NavBar(nav_items) + rv.Container(children=children or []) + + +@solara.component +def WizardApp( + title: str, + subtitle: str = "", + logo: LogoProps | None = None, + nav_items: list[NavItemProps] | None = None, + steps: list[StepProps] | None = None, +): + App( + title, + subtitle, + logo, + nav_items, + children=[Wizard(steps)], + ) diff --git a/src/aiidalab_qe/solara/components/header/__init__.py b/src/aiidalab_qe/solara/components/header/__init__.py new file mode 100644 index 000000000..96f4154cd --- /dev/null +++ b/src/aiidalab_qe/solara/components/header/__init__.py @@ -0,0 +1,8 @@ +from .header import Header +from .logo import Logo, LogoProps + +__all__ = [ + "Header", + "Logo", + "LogoProps", +] diff --git a/src/aiidalab_qe/solara/components/header/header.py b/src/aiidalab_qe/solara/components/header/header.py new file mode 100644 index 000000000..cc206d95d --- /dev/null +++ b/src/aiidalab_qe/solara/components/header/header.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import solara +from solara.alias import rv + +from .logo import Logo, LogoProps + + +@solara.component +def Header(title: str, subtitle: str = "", logo: LogoProps | None = None): + if logo: + Logo(**logo) + with rv.Container(class_="text-center"): + rv.Html( + tag="h1", + class_="display-5 fw-bold", + children=[title], + ) + if subtitle: + rv.Html( + tag="h2", + class_="lead mx-auto py-2 text-center", + children=[subtitle], + ) diff --git a/src/aiidalab_qe/solara/components/header/logo.py b/src/aiidalab_qe/solara/components/header/logo.py new file mode 100644 index 000000000..6f408ddec --- /dev/null +++ b/src/aiidalab_qe/solara/components/header/logo.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import solara +from solara.alias import rv + +LogoProps = dict[str, str] + + +@solara.component +def Logo(src: str, alt: str = ""): + rv.Img( + class_="d-block mx-auto", + src=src, + alt=alt, + width=100, + ) diff --git a/src/aiidalab_qe/solara/components/navbar/__init__.py b/src/aiidalab_qe/solara/components/navbar/__init__.py new file mode 100644 index 000000000..4915cd53a --- /dev/null +++ b/src/aiidalab_qe/solara/components/navbar/__init__.py @@ -0,0 +1,9 @@ +from .bar import NavBar +from .item import LinkNavItem, NavItem, NavItemProps + +__all__ = [ + "LinkNavItem", + "NavBar", + "NavItem", + "NavItemProps", +] diff --git a/src/aiidalab_qe/solara/components/navbar/bar.py b/src/aiidalab_qe/solara/components/navbar/bar.py new file mode 100644 index 000000000..3069b41ca --- /dev/null +++ b/src/aiidalab_qe/solara/components/navbar/bar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import solara +from solara.alias import rv + +from .item import LinkNavItem, NavItem, NavItemProps + + +@solara.component +def NavBar(items: list[NavItemProps]): + with rv.Container(class_="d-grid d-md-block mb-3 p-0 justify-content-center"): + for item in items: + LinkNavItem(**item) if "href" in item else NavItem(**item) diff --git a/src/aiidalab_qe/solara/components/navbar/item.py b/src/aiidalab_qe/solara/components/navbar/item.py new file mode 100644 index 000000000..7bc7acc31 --- /dev/null +++ b/src/aiidalab_qe/solara/components/navbar/item.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import solara + +NavItemProps = dict[str, str] + + +@solara.component +def NavItem(label: str = "", icon: str = "", **kwargs): + solara.Button( + class_="btn btn-primary btn-lg m-1 justify-content-start", + icon_name=f"mdi-{icon}", + outlined=True, + label=label, + **kwargs, + ) + + +@solara.component +def LinkNavItem(label: str = "", icon: str = "", href: str = "", **kwargs): + NavItem(label, icon, link=True, href=href, target="_blank", **kwargs) diff --git a/src/aiidalab_qe/solara/components/parameters/__init__.py b/src/aiidalab_qe/solara/components/parameters/__init__.py new file mode 100644 index 000000000..fa3f319d7 --- /dev/null +++ b/src/aiidalab_qe/solara/components/parameters/__init__.py @@ -0,0 +1,5 @@ +from .step import ParametersStep + +__all__ = [ + "ParametersStep", +] diff --git a/src/aiidalab_qe/solara/components/parameters/step.py b/src/aiidalab_qe/solara/components/parameters/step.py new file mode 100644 index 000000000..2ef8e1067 --- /dev/null +++ b/src/aiidalab_qe/solara/components/parameters/step.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import solara + +from ..wizard.step import onStateChange + + +@solara.component +def ParametersStep(on_state_change: onStateChange): + pass diff --git a/src/aiidalab_qe/solara/components/resources/__init__.py b/src/aiidalab_qe/solara/components/resources/__init__.py new file mode 100644 index 000000000..27c073380 --- /dev/null +++ b/src/aiidalab_qe/solara/components/resources/__init__.py @@ -0,0 +1,5 @@ +from .step import ResourcesStep + +__all__ = [ + "ResourcesStep", +] diff --git a/src/aiidalab_qe/solara/components/resources/step.py b/src/aiidalab_qe/solara/components/resources/step.py new file mode 100644 index 000000000..638204950 --- /dev/null +++ b/src/aiidalab_qe/solara/components/resources/step.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import solara + +from ..wizard.step import onStateChange + + +@solara.component +def ResourcesStep(on_state_change: onStateChange): + pass diff --git a/src/aiidalab_qe/solara/components/results/__init__.py b/src/aiidalab_qe/solara/components/results/__init__.py new file mode 100644 index 000000000..2f490703a --- /dev/null +++ b/src/aiidalab_qe/solara/components/results/__init__.py @@ -0,0 +1,5 @@ +from .step import ResultsStep + +__all__ = [ + "ResultsStep", +] diff --git a/src/aiidalab_qe/solara/components/results/step.py b/src/aiidalab_qe/solara/components/results/step.py new file mode 100644 index 000000000..c717c825f --- /dev/null +++ b/src/aiidalab_qe/solara/components/results/step.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import solara + +from ..wizard.step import onStateChange + + +@solara.component +def ResultsStep(on_state_change: onStateChange): + pass diff --git a/src/aiidalab_qe/solara/components/structure/__init__.py b/src/aiidalab_qe/solara/components/structure/__init__.py new file mode 100644 index 000000000..3bdee9292 --- /dev/null +++ b/src/aiidalab_qe/solara/components/structure/__init__.py @@ -0,0 +1,5 @@ +from .step import StructureStep + +__all__ = [ + "StructureStep", +] diff --git a/src/aiidalab_qe/solara/components/structure/step.py b/src/aiidalab_qe/solara/components/structure/step.py new file mode 100644 index 000000000..0f3dd9b68 --- /dev/null +++ b/src/aiidalab_qe/solara/components/structure/step.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import solara + +from ..wizard.step import onStateChange + + +@solara.component +def StructureStep(on_state_change: onStateChange): + pass diff --git a/src/aiidalab_qe/solara/components/submission/__init__.py b/src/aiidalab_qe/solara/components/submission/__init__.py new file mode 100644 index 000000000..203556fe7 --- /dev/null +++ b/src/aiidalab_qe/solara/components/submission/__init__.py @@ -0,0 +1,5 @@ +from .step import SubmissionStep + +__all__ = [ + "SubmissionStep", +] diff --git a/src/aiidalab_qe/solara/components/submission/step.py b/src/aiidalab_qe/solara/components/submission/step.py new file mode 100644 index 000000000..820cc7730 --- /dev/null +++ b/src/aiidalab_qe/solara/components/submission/step.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import solara + +from ..wizard.step import onStateChange + + +@solara.component +def SubmissionStep(on_state_change: onStateChange): + pass diff --git a/src/aiidalab_qe/solara/components/wizard/__init__.py b/src/aiidalab_qe/solara/components/wizard/__init__.py new file mode 100644 index 000000000..81f3a8fdb --- /dev/null +++ b/src/aiidalab_qe/solara/components/wizard/__init__.py @@ -0,0 +1,8 @@ +from .step import StepProps, WizardStep +from .wizard import Wizard + +__all__ = [ + "StepProps", + "Wizard", + "WizardStep", +] diff --git a/src/aiidalab_qe/solara/components/wizard/step.py b/src/aiidalab_qe/solara/components/wizard/step.py new file mode 100644 index 000000000..9d0e0f35a --- /dev/null +++ b/src/aiidalab_qe/solara/components/wizard/step.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import typing as t + +import solara + +from aiidalab_qe.solara.common.state import State + +onStateChange = t.Callable[[State], None] +QeAppWizardStep = t.Callable[[onStateChange], solara.Element] +StepProps = tuple[str, QeAppWizardStep] + + +@solara.component +def WizardStep( + step: QeAppWizardStep, + on_state_change: onStateChange, + confirmable: bool = True, +): + step(on_state_change) + if confirmable: + solara.Button( + label="Confirm", + color="success", + icon_name="check", + on_click=lambda: on_state_change(State.SUCCESS), + ) diff --git a/src/aiidalab_qe/solara/components/wizard/wizard.py b/src/aiidalab_qe/solara/components/wizard/wizard.py new file mode 100644 index 000000000..1782fab12 --- /dev/null +++ b/src/aiidalab_qe/solara/components/wizard/wizard.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import solara +from solara.alias import rv + +from aiidalab_qe.solara.common.state import BG_COLORS, STATE_ICONS, State + +from .step import StepProps, WizardStep + + +@solara.component +def Wizard(steps: list[StepProps]): + selected_index, set_selected_index = solara.use_state(None) + states, set_states = solara.use_state([State.INIT for _ in steps]) + + def on_selection(index: int): + set_selected_index(index) + + def on_state_change(i: int, new_state: State): + set_states([*states[:i], new_state, *states[i + 1 :]]) + if selected_index < len(steps) - 1: + set_selected_index(selected_index + 1) + + with rv.ExpansionPanels( + class_="accordion gap-1", + accordion=True, + v_model=selected_index, + on_v_model=on_selection, + hover=True, + ): + for i, (title, step) in enumerate(steps): + with rv.ExpansionPanel( + class_="accordion-item", + ): + with rv.ExpansionPanelHeader( + class_="accordion-header align-items-center justify-content-start", + style_=f"background-color: {BG_COLORS[states[i]]}", + ): + with rv.Container(class_="d-flex p-0"): + rv.Icon( + class_="me-3", + style_="margin-bottom: 1px", + children=[STATE_ICONS[states[i]]], + ) + rv.Text( + class_="align-self-end", + children=[f"Step {i + 1}: {title}"], + ) + with rv.ExpansionPanelContent(class_="accordion-collapse"): + WizardStep( + step=step, + on_state_change=lambda state, i=i: on_state_change(i, state), + confirmable=i < len(steps) - 1, + ) diff --git a/src/aiidalab_qe/solara/main.py b/src/aiidalab_qe/solara/main.py new file mode 100644 index 000000000..dfd7a61fa --- /dev/null +++ b/src/aiidalab_qe/solara/main.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from pathlib import Path + +import solara + +from .components import ( + ParametersStep, + ResourcesStep, + ResultsStep, + StructureStep, + SubmissionStep, + WizardApp, +) + + +@solara.component +def Page(): + with solara.Head(): + solara.Title("AiiDAlab QE app") + solara.Style(Path(__file__).parent / "assets/styles/css/main.css") + WizardApp( + title="The AiiDAlab Quantum ESPRESSO app", + subtitle="🎉 Happy computing 🎉", + logo={ + "src": "docs/source/_static/images/aiidalab_qe_logo.png", + "alt": "AiiDAlab Quantum ESPRESSO app logo", + }, + nav_items=[ + { + "label": "Getting started", + "icon": "rocket", + }, + { + "label": "About", + "icon": "information", + }, + { + "label": "Calculation history", + "icon": "format-list-bulleted-square", + "href": "./calculation_history.ipynb", + }, + { + "label": "Setup resources", + "icon": "database", + "href": "../home/code_setup.ipynb", + }, + { + "label": "New calculation", + "icon": "plus-circle", + "href": "./test.ipynb", + }, + ], + steps=[ + ( + "Select structure", + StructureStep, + ), + ( + "Configure the workflow", + ParametersStep, + ), + ( + "Choose computational resources", + ResourcesStep, + ), + ( + "Submit the workflow", + SubmissionStep, + ), + ( + "Status & results", + ResultsStep, + ), + ], + )