Repository: aiidalab/aiidalab-qe
Pull request #1196: Add `status` field to the plugin entry to show the maturity or release stage of the plugin
Author: login: superstar54
Created: 2025-03-05T10:02:40Z | Merged: 2025-03-06T15:02:35Z

This PR refactor the plugin manager:

- Add a PluginManager class, and import it inside the Jupyter notebook
- Add `status`, `category` and `requires_aiidalab_qe` fields.
- Add test for the plugin manager
- Read the `plugins.yaml` file from github instead of from local.
- Additionally, rename the `search_jobs` files to `calculation_history`


## Add `status` field


Each plugin has a **status badge** indicating its current development stage:  

| **Status**        | **Meaning** |
|-------------------|------------|
| 🟠 **Experimental** | The plugin is in an early stage of development and may be unstable. Expect rapid changes and potential issues. |
| 🟡 **Beta** | The plugin is functional and being tested, but some features may still be refined. |
| 🔵 **Stable** | The plugin is feature-complete, has undergone significant testing, and is considered reliable for most use cases. Minor updates or bug fixes may still occur. |
| 🟢 **Production** | The plugin is fully stable, widely tested, and officially recommended for all users. It follows strict maintenance and support policies. |

Here is an example of the UI:

![Screenshot from 2025-03-05 11-45-25](https://github.com/user-attachments/assets/c820d5f2-cc8f-4f1f-8fb0-384b71ea886f)

## Read the `plugins.yaml` file from github instead of from local.
In this way, a developer can continue adding new plugins after the app is released.

## Add `requires_aiidalab_qe` field
e.g., `requires_aiidalab_qe: '>=24.10.0'`
Disable the `install` button, if the aiidalab_qe is fulfilled.



## Add available plugins in step 2

![Screenshot from 2025-03-05 14-33-50](https://github.com/user-attachments/assets/ad9a99d8-242d-4a8e-b6e9-fefdf4177cf3)

---

Closes #827 

Linked issue #827: Let user know  available plugins quickly
Add a link to the properties section.


Or add a grey item for available plugins.
Linked issue comments: user: login: superstar54
created_at: 2024-10-04T11:42:18Z
body: > add a grey item for available plugins.

this seems not possible, because after the installation of a new plugin, we need to reload the page to active the python environment.
author_association: MEMBER

Diff:
diff --git a/calculation_history.ipynb b/calculation_history.ipynb
index 6ad5a9130..b6ded9a37 100644
--- a/calculation_history.ipynb
+++ b/calculation_history.ipynb
@@ -102,7 +102,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from aiidalab_qe.app.utils.search_jobs import CalculationHistory\n",
+    "from aiidalab_qe.app.utils.calculation_history import CalculationHistory\n",
     "\n",
     "calculation_history = CalculationHistory()\n",
     "calculation_history.main"
diff --git a/plugin_list.ipynb b/plugin_list.ipynb
deleted file mode 100644
index b91878ff2..000000000
--- a/plugin_list.ipynb
+++ /dev/null
@@ -1,354 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "vscode": {
-     "languageId": "javascript"
-    }
-   },
-   "outputs": [],
-   "source": [
-    "%%javascript\n",
-    "IPython.OutputArea.prototype._should_scroll = function(lines) {\n",
-    "    return false;\n",
-    "}\n",
-    "document.title='AiiDAlab QE | Manage plugins'"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "%%capture\n",
-    "from aiida import load_profile\n",
-    "\n",
-    "load_profile()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from aiidalab_widgets_base.utils.loaders import load_css\n",
-    "\n",
-    "load_css(css_path=\"src/aiidalab_qe/app/static/styles\")"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# AiiDAlab Quantum ESPRESSO - Plugin manager\n",
-    "\n",
-    "This page lets you manage the plugins of the AiiDAlab Quantum ESPRESSO app. You can find below all plugins available in the official [AiiDAlab Quantum ESPRESSO plugin registry](https://github.com/aiidalab/aiidalab-qe/blob/main/plugins.yaml) (click [here](https://aiidalab-qe.readthedocs.io/development/plugin_registry.html) to learn how to register a new plugin, if you are developing one). You can install and uninstall plugins from this page.\n",
-    "\n",
-    "\n",
-    "### Available plugins\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from pathlib import Path\n",
-    "\n",
-    "import yaml\n",
-    "\n",
-    "# Get the current working directory\n",
-    "cwd = Path.cwd()\n",
-    "# Define a relative path\n",
-    "relative_path = \"plugins.yaml\"\n",
-    "# Resolve the relative path to an absolute path\n",
-    "yaml_file = cwd / relative_path\n",
-    "\n",
-    "# Load the YAML content\n",
-    "with yaml_file.open(\"r\") as file:\n",
-    "    data = yaml.safe_load(file)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import subprocess\n",
-    "import sys\n",
-    "from threading import Thread\n",
-    "\n",
-    "import ipywidgets as ipw\n",
-    "from IPython.display import display\n",
-    "\n",
-    "\n",
-    "def is_package_installed(package_name):\n",
-    "    import importlib\n",
-    "\n",
-    "    package_name = package_name.replace(\"-\", \"_\")\n",
-    "    try:\n",
-    "        importlib.import_module(package_name)\n",
-    "    except ImportError:\n",
-    "        return False\n",
-    "    else:\n",
-    "        return True\n",
-    "\n",
-    "\n",
-    "def stream_output(process, output_widget):\n",
-    "    \"\"\"Reads output from the process and forwards it to the output widget.\"\"\"\n",
-    "    while True:\n",
-    "        output = process.stdout.readline()\n",
-    "        if process.poll() is not None and output == \"\":\n",
-    "            break\n",
-    "        if output:\n",
-    "            output_widget.value += f\"\"\"<div style=\"background-color: #3B3B3B; color: #FFFFFF;\">{output}</div>\"\"\"\n",
-    "\n",
-    "\n",
-    "def execute_command_with_output(\n",
-    "    command, output_widget, install_btn, remove_btn, action=\"install\"\n",
-    "):\n",
-    "    \"\"\"Execute a command and stream its output to the given output widget.\"\"\"\n",
-    "    output_widget.value = \"\"  # Clear the widget\n",
-    "    process = subprocess.Popen(\n",
-    "        command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1\n",
-    "    )\n",
-    "    # Create a thread to read the output stream and write it to the output widget\n",
-    "    thread = Thread(target=stream_output, args=(process, output_widget))\n",
-    "    thread.start()\n",
-    "    thread.join()  # Wait for the thread to finish\n",
-    "\n",
-    "    if process.returncode == 0 and action == \"install\":\n",
-    "        install_btn.disabled = True\n",
-    "        remove_btn.disabled = False\n",
-    "        return True\n",
-    "    elif process.returncode == 0 and action == \"remove\":\n",
-    "        install_btn.disabled = False\n",
-    "        remove_btn.disabled = True\n",
-    "        return True\n",
-    "    else:\n",
-    "        output_widget.value += \"\"\"<div style=\"background-color: #3B3B3B; color: #FF0000;\">Command failed.</div>\"\"\"\n",
-    "        return False\n",
-    "\n",
-    "\n",
-    "def install_package(\n",
-    "    package_name,\n",
-    "    pip,\n",
-    "    github,\n",
-    "    post_install,\n",
-    "    output_container,\n",
-    "    message_container,\n",
-    "    install_btn,\n",
-    "    remove_btn,\n",
-    "    accordion,\n",
-    "    index,\n",
-    "):\n",
-    "    if pip:\n",
-    "        command = [\"pip\", \"install\", pip, \"--user\"]\n",
-    "    else:\n",
-    "        command = [\"pip\", \"install\", \"git+\" + github, \"--user\"]\n",
-    "    message_container.value = (\n",
-    "        f\"\"\"<div style=\"color: #000000;\">Installing {package_name}...</div>\"\"\"\n",
-    "    )\n",
-    "    result = execute_command_with_output(\n",
-    "        command, output_container, install_btn, remove_btn\n",
-    "    )\n",
-    "    # Execute post install if defined in the plugin.yaml:\n",
-    "    if post_install:\n",
-    "        message_container.value += (\n",
-    "            \"\"\"<div style=\"color: #008000;\">Post installation step...</div>\"\"\"\n",
-    "        )\n",
-    "        command = [sys.executable, \"-m\", package_name.replace(\"-\", \"_\"), post_install]\n",
-    "        # Execute the command\n",
-    "        result = subprocess.run(command, capture_output=True, text=True, check=False)\n",
-    "    # if the package was installed successfully\n",
-    "    if result:\n",
-    "        message_container.value += \"\"\"<div style=\"color: #008000;\">Initiating test to load the plugin...</div>\"\"\"\n",
-    "        # Test plugin functionality\n",
-    "        command = [sys.executable, \"-m\", \"aiidalab_qe\", \"test-plugin\", package_name]\n",
-    "        # Execute the command\n",
-    "        result = subprocess.run(command, capture_output=True, text=True, check=False)\n",
-    "        if result.returncode == 0:\n",
-    "            # restart daemon\n",
-    "            message_container.value = (\n",
-    "                \"\"\"<div style=\"color: #008000;\">Loading plugin test passed.</div>\"\"\"\n",
-    "            )\n",
-    "            message_container.value += (\n",
-    "                \"\"\"<div style=\"color: #008000;\">Plugin installed successfully.</div>\"\"\"\n",
-    "            )\n",
-    "            accordion.set_title(index, f\"{accordion.get_title(index)[:-2]} ✅\")\n",
-    "            command = [\"verdi\", \"daemon\", \"restart\"]\n",
-    "            subprocess.run(command, capture_output=True, shell=False, check=False)\n",
-    "        else:\n",
-    "            # uninstall the package\n",
-    "            message_container.value = f\"\"\"<div style=\"color: #FF0000;\">The plugin '{package_name}' was installed successfully but plugin functionality test failed: {result.stderr}. </div>\"\"\"\n",
-    "            message_container.value += \"\"\"<div style=\"color: #FF0000;\">This may be due to compatibility issues with the current AiiDAlab QEApp version. Please contact the plugin author for further assistance.</div>\"\"\"\n",
-    "            message_container.value += \"\"\"<div style=\"color: #FF0000;\">To prevent potential issues, the plugin will now be uninstalled.</div>\"\"\"\n",
-    "            remove_package(\n",
-    "                package_name,\n",
-    "                output_container,\n",
-    "                message_container,\n",
-    "                install_btn,\n",
-    "                remove_btn,\n",
-    "                accordion,\n",
-    "                index,\n",
-    "            )\n",
-    "\n",
-    "\n",
-    "def remove_package(\n",
-    "    package_name,\n",
-    "    output_container,\n",
-    "    message_container,\n",
-    "    install_btn,\n",
-    "    remove_btn,\n",
-    "    accordion,\n",
-    "    index,\n",
-    "):\n",
-    "    message_container.value += (\n",
-    "        f\"\"\"<div style=\"color: #FF0000;\">Removing {package_name}...</div>\"\"\"\n",
-    "    )\n",
-    "    package_name = package_name.replace(\"-\", \"_\")\n",
-    "    command = [\"pip\", \"uninstall\", \"-y\", package_name]\n",
-    "    result = execute_command_with_output(\n",
-    "        command, output_container, install_btn, remove_btn, action=\"remove\"\n",
-    "    )\n",
-    "    if result:\n",
-    "        message_container.value += f\"\"\"<div style=\"color: #008000;\">{package_name} removed successfully.</div>\"\"\"\n",
-    "        accordion.set_title(index, f\"{accordion.get_title(index)[:-2]} ☐\")\n",
-    "        command = [\"verdi\", \"daemon\", \"restart\"]\n",
-    "        subprocess.run(command, capture_output=True, shell=False, check=False)\n",
-    "\n",
-    "\n",
-    "def run_remove_button(\n",
-    "    package_name,\n",
-    "    output_container,\n",
-    "    message_container,\n",
-    "    install_btn,\n",
-    "    remove_btn,\n",
-    "    accordion,\n",
-    "    index,\n",
-    "):\n",
-    "    message_container.value = \"\"\n",
-    "    remove_package(\n",
-    "        package_name,\n",
-    "        output_container,\n",
-    "        message_container,\n",
-    "        install_btn,\n",
-    "        remove_btn,\n",
-    "        accordion,\n",
-    "        index,\n",
-    "    )\n",
-    "\n",
-    "\n",
-    "accordion = ipw.Accordion()\n",
-    "\n",
-    "for i, (plugin_name, plugin_data) in enumerate(data.items()):\n",
-    "    installed = is_package_installed(plugin_name)\n",
-    "\n",
-    "    # Output container with customized styling\n",
-    "    output_container = ipw.HTML(\n",
-    "        value=\"\"\"\n",
-    "        <div style=\"background-color: #3B3B3B; color: #FFFFFF; height: 100%; overflow: auto;\">\n",
-    "        </div>\n",
-    "        \"\"\",\n",
-    "        layout=ipw.Layout(\n",
-    "            max_height=\"250px\", overflow=\"auto\", border=\"2px solid #CCCCCC\"\n",
-    "        ),\n",
-    "    )\n",
-    "    # Output container with customized styling\n",
-    "    message_container = ipw.HTML(\n",
-    "        value=\"\"\"\n",
-    "        <div style=\"color: #000000; height: 100%; overflow: auto;\">\n",
-    "        </div>\n",
-    "        \"\"\",\n",
-    "        layout=ipw.Layout(\n",
-    "            max_height=\"250px\", overflow=\"auto\", border=\"2px solid #CCCCCC\"\n",
-    "        ),\n",
-    "    )\n",
-    "\n",
-    "    details = (\n",
-    "        f\"Author: {plugin_data.get('author', 'N/A')}<br>\"\n",
-    "        f\"Description: {plugin_data.get('description', 'No description available')}<br>\"\n",
-    "    )\n",
-    "    if \"documentation\" in plugin_data:\n",
-    "        details += f\"Documentation: <a href='{plugin_data['documentation']}' target='_blank'>Visit</a><br>\"\n",
-    "    if \"github\" in plugin_data:\n",
-    "        details += (\n",
-    "            f\"Github: <a href='{plugin_data.get('github')}' target='_blank'>Visit</a>\"\n",
-    "        )\n",
-    "\n",
-    "    install_btn = ipw.Button(\n",
-    "        description=\"Install\", button_style=\"success\", disabled=installed\n",
-    "    )\n",
-    "    remove_btn = ipw.Button(\n",
-    "        description=\"Remove\", button_style=\"danger\", disabled=not installed\n",
-    "    )\n",
-    "\n",
-    "    install_btn.on_click(\n",
-    "        lambda _btn,\n",
-    "        pn=plugin_name,\n",
-    "        pip=plugin_data.get(\"pip\", None),  # noqa: B008\n",
-    "        github=plugin_data.get(\"github\", \"\"),  # noqa: B008\n",
-    "        post=plugin_data.get(\"post_install\", None),  # noqa: B008\n",
-    "        oc=output_container,\n",
-    "        mc=message_container,\n",
-    "        ib=install_btn,\n",
-    "        rb=remove_btn,\n",
-    "        ac=accordion,\n",
-    "        index=i: install_package(pn, pip, github, post, oc, mc, ib, rb, ac, index)\n",
-    "    )\n",
-    "    remove_btn.on_click(\n",
-    "        lambda _btn,\n",
-    "        pn=plugin_name,\n",
-    "        oc=output_container,\n",
-    "        mc=message_container,\n",
-    "        ib=install_btn,\n",
-    "        rb=remove_btn,\n",
-    "        ac=accordion,\n",
-    "        index=i: run_remove_button(pn, oc, mc, ib, rb, ac, index)\n",
-    "    )\n",
-    "\n",
-    "    box = ipw.VBox(\n",
-    "        [\n",
-    "            ipw.HTML(details),\n",
-    "            ipw.HBox([install_btn, remove_btn]),\n",
-    "            message_container,\n",
-    "            output_container,\n",
-    "        ]\n",
-    "    )\n",
-    "\n",
-    "    title_with_icon = f\"{plugin_data.get('title')} {'✅' if installed else '☐'}\"\n",
-    "    accordion.set_title(i, title_with_icon)\n",
-    "    accordion.children = [*accordion.children, box]\n",
-    "\n",
-    "display(accordion)"
-   ]
-  }
- ],
- "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/plugin_manager.ipynb b/plugin_manager.ipynb
new file mode 100644
index 000000000..cf4652723
--- /dev/null
+++ b/plugin_manager.ipynb
@@ -0,0 +1,102 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "vscode": {
+     "languageId": "javascript"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "%%javascript\n",
+    "IPython.OutputArea.prototype._should_scroll = function(lines) {\n",
+    "    return false;\n",
+    "}\n",
+    "document.title='AiiDAlab QE | Manage plugins'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%%capture\n",
+    "from aiida import load_profile\n",
+    "\n",
+    "load_profile()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from aiidalab_widgets_base.utils.loaders import load_css\n",
+    "\n",
+    "load_css(css_path=\"src/aiidalab_qe/app/static/styles\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# AiiDAlab Quantum ESPRESSO - Plugin manager"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import ipywidgets as ipw\n",
+    "from importlib_resources import files\n",
+    "from jinja2 import Environment\n",
+    "\n",
+    "from aiidalab_qe.app.static import templates\n",
+    "\n",
+    "env = Environment()\n",
+    "guide_template = files(templates).joinpath(\"plugin_manager.jinja\").read_text()\n",
+    "guide = ipw.HTML(env.from_string(guide_template).render())\n",
+    "guide"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from aiidalab_qe.app.utils.plugin_manager import PluginManager\n",
+    "\n",
+    "pm = PluginManager()\n",
+    "pm.display_ui()"
+   ]
+  }
+ ],
+ "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/plugins.yaml b/plugins.yaml
index 5362eb935..9b161ee0c 100644
--- a/plugins.yaml
+++ b/plugins.yaml
@@ -1,45 +1,64 @@
 ---
-aiida-bader:
-    title: Bader charge analysis (aiida-bader)
-    description: Perform Bader charge analysis of the electronic charge density
-    author: Xing Wang
-    github: https://github.com/superstar54/aiida-bader
-    documentation: https://aiida-bader.readthedocs.io/
-    pip: aiida-bader
-
 aiidalab-qe-vibroscopy:
     title: Phonons and IR/Raman (aiidalab-qe-vibroscopy)
     description: Plugin to compute phonons, IR/Raman spectra, Inelastic Neutron Scattering of materials via finite displacement and finite field approach.
     author: Miki Bonacci, Andres Ortega Guerrero, Lorenzo Bastonero and Nicola Marzari
     pip: aiidalab-qe-vibroscopy~=1.1.3
     post_install: setup-phonopy
+    status: stable
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
 
 aiidalab-qe-muon:
     title: Muon spectroscopy (aiidalab-qe-muon)
     description: Compute properties to assist µSR experiments, such as muon stopping sites and related properties (local fields and polarization signals).
     author: Miki Bonacci, Ifeanyi J. Onuorah, Pietro Bonfa', Giovanni Pizzi and Roberto de Renzi
     github: https://github.com/mikibonacci/aiidalab-qe-muon
+    status: stable
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
 
 aiida-qe-xspec:
     title: Core-level spectroscopy (aiida-qe-xspec)
     description: Core-level spectroscopy calculations with Quantum ESPRESSO.
-    author: Xing Wang, Peter Gillespie
+    author: Peter Gillespie, Michael A. Hernandez-Bertran, Giovanni Pizzi, Deborah Prezzi, Xing Wang (ordered by last name alphabetically)
     github: https://github.com/aiidaplugins/aiida-qe-xspec
     documentation: https://aiida-qe-xspec.readthedocs.io/
     pip: aiida-qe-xspec
+    status: stable
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
+
+aiida-bader:
+    title: Bader charge analysis (aiida-bader)
+    description: Perform Bader charge analysis of the electronic charge density
+    author: Xing Wang
+    github: https://github.com/superstar54/aiida-bader
+    documentation: https://aiida-bader.readthedocs.io/
+    pip: aiida-bader
+    post_install: post-install
+    status: stable
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
 
 aiidalab-qe-wannier90:
     title: Wannier functions (aiidalab-qe-wannier90)
     description: A plugin to compute Wannier functions using Quantum ESPRESSO and the Wannier90 code.
-    author: Xing Wang, Junfeng Qiao and Giovanni Pizzi
+    author: Xing Wang, Junfeng Qiao, Nicola Marzari and Giovanni Pizzi
     github: https://github.com/superstar54/aiidalab-qe-wannier90
     documentation: https://aiidalab-qe-wannier90.readthedocs.io/
     pip: aiidalab-qe-wannier90
+    status: beta
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
 
 aiidalab-qe-hp:
-    title: Habbard parameter (aiidalab-qe-hp)
-    description: A plugin to compute Habbard parameter with Quantum ESPRESSO.
-    author: Xing Wang, Iurii Timrov and Giovanni Pizzi
+    title: Hubbard parameters (aiidalab-qe-hp)
+    description: A plugin to compute Hubbard parameters with Quantum ESPRESSO.
+    author: Xing Wang, Lorenzo Bastonero and Iurii Timrov
     github: https://github.com/superstar54/aiidalab-qe-hp
     documentation: https://aiidalab-qe-hp.readthedocs.io/
     pip: aiidalab-qe-hp
+    status: beta
+    category: calculation
+    requires_aiidalab_qe: '>=24.10.0'
diff --git a/src/aiidalab_qe/app/configuration/__init__.py b/src/aiidalab_qe/app/configuration/__init__.py
index 17d932b3e..7071c3dec 100644
--- a/src/aiidalab_qe/app/configuration/__init__.py
+++ b/src/aiidalab_qe/app/configuration/__init__.py
@@ -14,6 +14,7 @@
     ConfigurationSettingsModel,
     ConfigurationSettingsPanel,
 )
+from aiidalab_qe.common.widgets import LinkButton
 from aiidalab_qe.common.wizard import QeConfirmableDependentWizardStep
 
 from .advanced import (
@@ -67,13 +68,15 @@ def __init__(self, model: ConfigurationStepModel, **kwargs):
             "advanced": self.advanced_settings,
         }
 
-        self.property_children = []
-
+        self.installed_property_children = []
+        self.not_installed_property_children = []
         self._fetch_plugin_calculation_settings()
 
     def _render(self):
         super()._render()
 
+        self._fetch_not_installed_property()
+
         # RelaxType: degrees of freedom in geometry optimization
         self.relax_type_help = ipw.HTML()
         ipw.dlink(
@@ -99,12 +102,35 @@ def _render(self):
             "selected_index",
         )
 
+        self.install_new_plugin_button = LinkButton(
+            description="Plugin store",
+            link="plugin_manager.ipynb",
+            icon="puzzle-piece",  # More intuitive icon
+            button_style="primary",  # Keeps it prominent
+            tooltip="Browse and install additional plugins from the Plugin Store",
+        )
+
         self.sub_steps = ipw.Accordion(
             children=[
                 ipw.VBox(
                     children=[
                         InAppGuide(identifier="properties-selection"),
-                        *self.property_children,
+                        *self.installed_property_children,
+                        ipw.HTML("<hr>"),
+                        ipw.HTML(
+                            value="""
+                            <p style="font-size:14px; line-height:1.6;">
+                                The following additional property calculations are available in the
+                                <b>Plugin registry</b> but are currently disabled because the required plugins are not installed.
+                            </p>
+                            <p style="font-size:14px; line-height:1.6;">
+                                To enable them, please visit the <b>Plugin store</b> and install the necessary plugins.
+                            </p>
+                            """,
+                            layout=ipw.Layout(margin="10px 0px"),
+                        ),
+                        self.install_new_plugin_button,
+                        *self.not_installed_property_children,
                     ]
                 ),
                 ipw.VBox(
@@ -182,6 +208,34 @@ def _update_state(self, _=None):
         else:
             self.state = self.State.INIT
 
+    def _fetch_not_installed_property(self, plugin_config_source=None):
+        from aiidalab_qe.app.utils.plugin_manager import (
+            DEFAULT_PLUGIN_CONFIG_SOURCE,
+            PluginManager,
+            is_package_installed,
+        )
+
+        self.not_installed_property_children = []
+
+        plugin_config_source = plugin_config_source or DEFAULT_PLUGIN_CONFIG_SOURCE
+        plugin_manager = PluginManager(plugin_config_source)
+        for plugin_name, plugin_data in plugin_manager.data.items():
+            if (
+                plugin_data.get("category", "calculation").lower() != "calculation"
+            ):  # Ignore non-property plugins
+                continue
+
+            is_installed = is_package_installed(plugin_name)
+            if not is_installed:
+                checkbox = ipw.Checkbox(
+                    value=False,
+                    description=plugin_data["title"],
+                    disabled=True,
+                    indent=False,
+                    style={"description_width": "initial"},
+                )
+                self.not_installed_property_children.append(checkbox)
+
     def _fetch_plugin_calculation_settings(self):
         outlines = get_entry_items("aiidalab_qe.properties", "outline")
         entries = get_entry_items("aiidalab_qe.properties", "configuration")
@@ -222,7 +276,7 @@ def toggle_plugin(change, identifier=identifier, model=model, info=info):
                 "include",
             )
 
-            self.property_children.append(
+            self.installed_property_children.append(
                 ipw.HBox(
                     children=[
                         outline,
diff --git a/src/aiidalab_qe/app/static/templates/plugin_manager.jinja b/src/aiidalab_qe/app/static/templates/plugin_manager.jinja
new file mode 100644
index 000000000..2719a6684
--- /dev/null
+++ b/src/aiidalab_qe/app/static/templates/plugin_manager.jinja
@@ -0,0 +1,58 @@
+<div class="guide">
+    <h2>How to use this page</h2>
+    <p>
+        This page lets you manage the plugins of the AiiDAlab Quantum ESPRESSO app.
+        Below, you will find all available plugins from the
+        <a href="https://github.com/aiidalab/aiidalab-qe/blob/main/plugins.yaml" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            official AiiDAlab Quantum ESPRESSO plugin registry
+        </a>.
+    </p>
+    <p>
+        If you install a new plugin, you may need to <strong>refresh the calculation page</strong>
+        for the newly installed functionality to appear.
+    </p>
+    <p>
+        If you are developing a new plugin and would like to share it with the community,
+        <a href="https://aiidalab-qe.readthedocs.io/development/plugin_registry.html" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            click here
+        </a>
+        to learn how to register it in the official plugin registry.
+    </p>
+    <p>
+        You can install and uninstall plugins directly from this page.
+    </p>
+
+    <h3 style="margin-top: 20px;">Plugin status explanation</h3>
+    <p>Each plugin has a <strong>status badge</strong> indicating its development stage:</p>
+
+    <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
+        <thead>
+            <tr style="background-color: #f4f4f4;">
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Status</th>
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Meaning</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟠 <strong>Experimental</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is in an early stage of development and may be unstable. Expect rapid changes and potential issues.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟡 <strong>Beta</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is functional and being tested but some features may still be refined.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🔵 <strong>Stable</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is feature-complete, has undergone significant testing, and is considered reliable for most use cases. Minor updates or bug fixes may still occur.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟢 <strong>Production</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is fully stable, widely tested, and officially recommended for all users. It follows strict maintenance and support policies.</td>
+            </tr>
+        </tbody>
+    </table>
+
+    <h3 style="margin-top: 20px;">Available plugins</h3>
+</div>
diff --git a/src/aiidalab_qe/app/utils/search_jobs.py b/src/aiidalab_qe/app/utils/calculation_history.py
similarity index 100%
rename from src/aiidalab_qe/app/utils/search_jobs.py
rename to src/aiidalab_qe/app/utils/calculation_history.py
diff --git a/src/aiidalab_qe/app/utils/plugin_manager.py b/src/aiidalab_qe/app/utils/plugin_manager.py
new file mode 100644
index 000000000..123f8262c
--- /dev/null
+++ b/src/aiidalab_qe/app/utils/plugin_manager.py
@@ -0,0 +1,472 @@
+"""
+plugin_manager.py
+
+Module that provides a plugin management interface, allowing the user to
+install and remove Python-based AiiDAlab plugins, with real-time streaming
+of command output in a Jupyter environment.
+"""
+
+import importlib
+import subprocess
+import sys
+from threading import Thread
+
+import ipywidgets as ipw
+import requests
+import yaml
+from IPython.display import display
+from packaging.specifiers import SpecifierSet
+from packaging.version import parse
+
+# Define badge colors based on status
+COLOR_MAP = {
+    "experimental": "#FF8C00",  # 🟠 Orange - Early development
+    "beta": "#FFA500",  # 🟡 Darker Orange - Testing phase
+    "stable": "#1E90FF",  # 🔵 Dodger Blue - Well-tested, feature-complete
+    "production": "#008000",  # 🟢 Green - Fully stable and recommended
+    "deprecated": "#FF0000",  # 🔴 Red - No longer maintained
+    "archived": "#808080",  # ⚪ Grey - Retained for reference, no updates
+}
+DEFAULT_PLUGIN_CONFIG_SOURCE = (
+    "https://raw.githubusercontent.com/aiidalab/aiidalab-qe/main/plugins.yaml"
+)
+
+
+def get_aiidalab_qe_version() -> str:
+    """
+    Get the installed version of aiidalab_qe.
+    Returns 'unknown' if the package is not installed.
+    """
+    import aiidalab_qe
+
+    try:
+        return aiidalab_qe.__version__
+    except AttributeError:
+        return "unknown"
+
+
+INSTALLED_AIIDA_QE_VERSION = get_aiidalab_qe_version()
+
+
+def is_version_compatible(required_version: str) -> bool:
+    """
+    Check if the installed aiidalab_qe version satisfies the required version constraint.
+    This function explicitly allows pre-release versions if they match the specifier.
+    """
+    if not required_version:
+        return True
+
+    if INSTALLED_AIIDA_QE_VERSION == "unknown":
+        return False  # aiidalab_qe is not installed
+
+    try:
+        specifier = SpecifierSet(required_version)  # Handles constraints like '>=24.10'
+        installed_version = parse(INSTALLED_AIIDA_QE_VERSION)
+
+        # If the installed version is a pre-release, allow it if it matches the specifier
+        if installed_version.is_prerelease:
+            return installed_version in specifier or specifier.contains(
+                installed_version, prereleases=True
+            )
+        else:
+            return installed_version in specifier
+
+    except Exception as e:
+        print(f"Warning: Could not parse version requirement '{required_version}': {e}")
+        return False
+
+
+def fetch_plugins_from_github(url: str) -> dict:
+    """
+    Fetch the plugins.yaml file from GitHub and parse it into a dictionary.
+
+    :param url: URL of the plugins.yaml file.
+    :return: Parsed dictionary of plugins data.
+    """
+    import requests
+
+    try:
+        response = requests.get(url, timeout=10)
+        response.raise_for_status()  # Raise error if request fails
+        return yaml.safe_load(response.text)
+    except requests.RequestException as e:
+        print(f"⚠️ Warning: Failed to fetch plugins.yaml from GitHub: {e}")
+        return {}
+
+
+def is_package_installed(package_name: str) -> bool:
+    """
+    Check if a given Python package is already installed.
+    """
+    package_name = package_name.replace("-", "_")
+    try:
+        importlib.import_module(package_name)
+    except ImportError:
+        return False
+    else:
+        return True
+
+
+def stream_output(process: subprocess.Popen, output_widget: ipw.HTML) -> None:
+    """
+    Reads output from the process and forwards it to the output widget.
+    """
+    while True:
+        output = process.stdout.readline()
+        if process.poll() is not None and output == "":
+            break
+        if output:
+            output_widget.value += f"""<div style="background-color: #3B3B3B; color: #FFFFFF;">{output}</div>"""
+
+
+def execute_command_with_output(
+    command: list,
+    output_widget: ipw.HTML,
+    install_btn: ipw.Button,
+    remove_btn: ipw.Button,
+    action: str = "install",
+) -> bool:
+    """
+    Execute a shell command and stream its output to the provided widget.
+    """
+    output_widget.value = ""  # Clear the widget before running the command
+    process = subprocess.Popen(
+        command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
+    )
+
+    thread = Thread(target=stream_output, args=(process, output_widget))
+    thread.start()
+    thread.join()
+
+    if process.returncode == 0 and action == "install":
+        install_btn.disabled = True
+        remove_btn.disabled = False
+        return True
+    elif process.returncode == 0 and action == "remove":
+        install_btn.disabled = False
+        remove_btn.disabled = True
+        return True
+    else:
+        output_widget.value += """<div style="background-color: #3B3B3B; color: #FF0000;">Command failed.</div>"""
+        return False
+
+
+def remove_package(
+    package_name: str,
+    output_container: ipw.HTML,
+    message_container: ipw.HTML,
+    install_btn: ipw.Button,
+    remove_btn: ipw.Button,
+    accordion: ipw.Accordion,
+    index: int,
+) -> None:
+    """
+    Remove a plugin package via pip uninstall.
+    """
+    # Show the containers when user starts the remove action.
+    message_container.layout.display = "block"
+    output_container.layout.display = "block"
+
+    message_container.value += (
+        f"""<div style="color: #FF0000;">Removing {package_name}...</div>"""
+    )
+    normalized_name = package_name.replace("-", "_")
+    command = ["pip", "uninstall", "-y", normalized_name]
+    result = execute_command_with_output(
+        command, output_container, install_btn, remove_btn, action="remove"
+    )
+
+    if result:
+        message_container.value += f"""<div style="color: #008000;">{package_name} removed successfully.</div>"""
+        # Remove the checkmark from the Accordion title
+        title = accordion.get_title(index)
+        if title.endswith("✅"):
+            new_title = title[:-2] + "☐"
+            accordion.set_title(index, new_title)
+
+        # Attempt to restart AiiDA daemon
+        command = ["verdi", "daemon", "restart"]
+        subprocess.run(command, capture_output=True, check=False)
+
+
+def install_package(
+    package_name: str,
+    pip_install: str,
+    github: str,
+    post_install: str,
+    output_container: ipw.HTML,
+    message_container: ipw.HTML,
+    install_btn: ipw.Button,
+    remove_btn: ipw.Button,
+    accordion: ipw.Accordion,
+    index: int,
+) -> None:
+    """
+    Install a plugin package from pip or GitHub, then optionally run a post-install command
+    and test the plugin.
+    """
+    # Show the containers when user starts the install action.
+    message_container.layout.display = "block"
+    output_container.layout.display = "block"
+
+    if pip_install:
+        command = ["pip", "install", pip_install, "--user"]
+    else:
+        command = ["pip", "install", "git+" + github, "--user"]
+
+    message_container.value = (
+        f"""<div style="color: #000000;">Installing {package_name}...</div>"""
+    )
+    install_result = execute_command_with_output(
+        command, output_container, install_btn, remove_btn
+    )
+
+    # If post_install is defined, attempt to run that command
+    if install_result and post_install:
+        message_container.value += (
+            """<div style="color: #008000;">Post-install step in progress...</div>"""
+        )
+        cmd = [sys.executable, "-m", package_name.replace("-", "_"), post_install]
+        result = subprocess.run(cmd, capture_output=True, text=True, check=False)
+        if result.returncode != 0:
+            message_container.value += f"""
+                <div style="color: #FF0000;">
+                    Post-install command failed: {result.stderr}
+                </div>
+            """
+
+    # Test the plugin functionality if install was successful
+    if install_result:
+        message_container.value += (
+            """<div style="color: #008000;">Testing plugin loading...</div>"""
+        )
+        cmd = [sys.executable, "-m", "aiidalab_qe", "test-plugin", package_name]
+        test_result = subprocess.run(cmd, capture_output=True, text=True, check=False)
+
+        if test_result.returncode == 0:
+            message_container.value += (
+                """<div style="color: #008000;">Plugin test passed.</div>"""
+            )
+            message_container.value += (
+                """<div style="color: #008000;">Plugin installed successfully.</div>"""
+            )
+
+            # Update Accordion title with a checkmark
+            title = accordion.get_title(index)
+            if title.endswith("☐"):
+                new_title = title[:-1] + "✅"
+                accordion.set_title(index, new_title)
+
+            # Restart daemon
+            daemon_cmd = ["verdi", "daemon", "restart"]
+            subprocess.run(daemon_cmd, capture_output=True, check=False)
+
+        else:
+            message_container.value += f"""
+                <div style="color: #FF0000;">
+                    The plugin '{package_name}' was installed but failed functionality test:
+                    {test_result.stderr}.
+                </div>
+            """
+            message_container.value += """
+                <div style="color: #FF0000;">
+                    This may be due to compatibility issues with the current AiiDAlab QEApp.
+                    Please contact the plugin author for assistance.
+                </div>
+            """
+            message_container.value += """
+                <div style="color: #FF0000;">
+                    The plugin will now be uninstalled to prevent issues.
+                </div>
+            """
+            remove_package(
+                package_name,
+                output_container,
+                message_container,
+                install_btn,
+                remove_btn,
+                accordion,
+                index,
+            )
+
+
+class PluginManager:
+    """
+    A manager class that reads a plugin configuration file (YAML),
+    and creates an interactive Accordion UI for installing/uninstalling
+    those plugins in a Jupyter environment.
+    """
+
+    def __init__(self, config_source: str = DEFAULT_PLUGIN_CONFIG_SOURCE):
+        """
+        Initialize the PluginManager with a path to a YAML config or a URL.
+
+        :param config_source: Either a local YAML file path or a URL to a remote YAML file.
+        """
+        self.config_source = config_source
+        self.data = self._load_config()
+        self.accordion = ipw.Accordion()
+
+    def _load_config(self) -> dict:
+        """
+        Load and parse the YAML plugin configuration from a local file or a URL.
+        """
+        if self.config_source.startswith("http"):  # Detect if it's a URL
+            return self._fetch_remote_config(self.config_source)
+        else:  # Assume it's a local file
+            return self._fetch_local_config(self.config_source)
+
+    def _fetch_remote_config(self, url: str) -> dict:
+        """
+        Fetch the plugins.yaml file from GitHub and parse it into a dictionary.
+
+        :param url: URL of the plugins.yaml file.
+        :return: Parsed dictionary of plugins data.
+        """
+        try:
+            response = requests.get(url, timeout=10)
+            response.raise_for_status()  # Raise an error if request fails
+            return yaml.safe_load(response.text)
+        except requests.RequestException as e:
+            print(f"⚠️ Warning: Failed to fetch plugins.yaml from GitHub: {e}")
+            return {}
+
+    def _fetch_local_config(self, file_path: str) -> dict:
+        """
+        Load plugins.yaml from a local file.
+
+        :param file_path: Path to the YAML file.
+        :return: Parsed dictionary of plugins data.
+        """
+        try:
+            with open(file_path) as file:
+                return yaml.safe_load(file)
+        except (FileNotFoundError, yaml.YAMLError) as e:
+            print(f"⚠️ Warning: Failed to load local plugins.yaml: {e}")
+            return {}
+
+    def _build_ui(self) -> None:
+        """
+        Build the Accordion UI based on the loaded plugin data.
+        """
+        for i, (plugin_name, plugin_data) in enumerate(self.data.items()):
+            installed = is_package_installed(plugin_name)
+            required_version = plugin_data.get("requires_aiidalab_qe", None)
+            compatible = is_version_compatible(required_version)
+
+            # Create message and output containers
+            output_container = ipw.HTML(
+                value="",
+                layout=ipw.Layout(
+                    max_height="250px",
+                    overflow="auto",
+                    border="2px solid #CCCCCC",
+                    display="none",
+                ),
+            )
+            message_container = ipw.HTML(
+                value="",
+                layout=ipw.Layout(
+                    max_height="250px",
+                    overflow="auto",
+                    border="2px solid #CCCCCC",
+                    display="none",
+                ),
+            )
+
+            status_value = plugin_data.get("status", "").strip().lower()
+            badge_color = COLOR_MAP.get(status_value, "#666666")
+            display_text = status_value.capitalize() if status_value else "N/A"
+
+            badge_html = f"""
+                <span style="background-color: {badge_color}; color: #FFFFFF;
+                            border-radius: 4px; padding: 2px 6px;">
+                    {display_text}
+                </span>
+            """
+
+            # Display AIIDA version requirement if any
+            version_message = ""
+            if not compatible:
+                version_message = f"""
+                <div style="color: #FF0000;">
+                    ⚠️ This plugin requires aiidalab_qe >= {required_version}, but you have {INSTALLED_AIIDA_QE_VERSION}.
+                </div>
+                """
+
+            # Build plugin description details
+            details = f"""
+                <b>Author:</b> {plugin_data.get('author', 'N/A')}<br>
+                <b>Description:</b> {plugin_data.get('description', 'No description available')}<br>
+                <b>Status:</b> {badge_html}<br>
+                {version_message}
+            """
+
+            if "documentation" in plugin_data:
+                details += f"📖 <b>Documentation:</b> <a href='{plugin_data['documentation']}' target='_blank'>Visit</a><br>"
+            if "github" in plugin_data:
+                details += f"💻 <b>Github:</b> <a href='{plugin_data.get('github')}' target='_blank'>Visit</a>"
+
+            # Create install/remove buttons
+            install_btn = ipw.Button(
+                description="Install",
+                button_style="success",
+                disabled=(
+                    not compatible or installed
+                ),  # Disable if not compatible or already installed
+            )
+            remove_btn = ipw.Button(
+                description="Remove",
+                button_style="danger",
+                disabled=not installed,
+            )
+
+            # Attach callbacks
+            pip_data = plugin_data.get("pip", None)
+            github_data = plugin_data.get("github", "")
+            post_install_data = plugin_data.get("post_install", None)
+            install_btn.on_click(
+                lambda _btn,
+                pn=plugin_name,
+                pip=pip_data,
+                gh=github_data,
+                post=post_install_data,
+                oc=output_container,
+                mc=message_container,
+                ib=install_btn,
+                rb=remove_btn,
+                ac=self.accordion,
+                idx=i: install_package(pn, pip, gh, post, oc, mc, ib, rb, ac, idx)
+            )
+            remove_btn.on_click(
+                lambda _btn,
+                pn=plugin_name,
+                oc=output_container,
+                mc=message_container,
+                ib=install_btn,
+                rb=remove_btn,
+                ac=self.accordion,
+                idx=i: remove_package(pn, oc, mc, ib, rb, ac, idx)
+            )
+
+            # Create layout for each plugin
+            box = ipw.VBox(
+                [
+                    ipw.HTML(details),
+                    ipw.HBox([install_btn, remove_btn]),
+                    message_container,
+                    output_container,
+                ]
+            )
+
+            # Define the title with checkmark if installed
+            title_with_icon = f"{plugin_data.get('title')} {'✅' if installed else ''}"
+            self.accordion.set_title(i, title_with_icon)
+            self.accordion.children = [*self.accordion.children, box]
+
+    def display_ui(self) -> None:
+        """
+        Display the Accordion UI in a Jupyter notebook.
+        """
+        self._build_ui()
+        display(self.accordion)
diff --git a/start.py b/start.py
index 46a9ed689..9c23d9ded 100644
--- a/start.py
+++ b/start.py
@@ -36,7 +36,7 @@ def get_start_widget(appbase, jupbase, notebase):  # noqa: ARG001
                 </a>
                 <a
                     class="feature"
-                    href="{appbase}/plugin_list.ipynb"
+                    href="{appbase}/plugin_manager.ipynb"
                     target="_blank">
                     <img
                         class="feature-logo"
diff --git a/tests/test_calculation_history.py b/tests/test_calculation_history.py
index 10a7c8196..a7e5f1107 100644
--- a/tests/test_calculation_history.py
+++ b/tests/test_calculation_history.py
@@ -1,5 +1,5 @@
 def test_calculation_history(sssp, generate_qeapp_workchain):
-    from aiidalab_qe.app.utils.search_jobs import CalculationHistory
+    from aiidalab_qe.app.utils.calculation_history import CalculationHistory
 
     workchain = generate_qeapp_workchain()
     workchain.node.seal()
diff --git a/tests/test_configure.py b/tests/test_configure.py
index db22fbb0b..5b78328df 100644
--- a/tests/test_configure.py
+++ b/tests/test_configure.py
@@ -67,7 +67,7 @@ def test_reminder_info():
     bands_info = next(
         (
             child.children[1]
-            for child in config.property_children
+            for child in config.installed_property_children
             if "Electronic band structure" in child.children[0].title
         ),
         None,
@@ -78,3 +78,14 @@ def test_reminder_info():
     assert bands_info.value == "Customize bands settings in <b>Step 2.2</b> if needed"
     bands_model.include = False
     assert bands_info.value == ""
+
+
+def test_not_installed_property_children():
+    import os
+
+    current_file = os.path.abspath(__file__)
+    plugin_file = os.path.join(os.path.dirname(current_file), "../plugins.yaml")
+    model = ConfigurationStepModel()
+    config = ConfigureQeAppWorkChainStep(model=model)
+    config._fetch_not_installed_property(str(plugin_file))
+    assert len(config.not_installed_property_children) > 0
diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py
new file mode 100644
index 000000000..59db0fa8a
--- /dev/null
+++ b/tests/test_plugin_manager.py
@@ -0,0 +1,55 @@
+import tempfile
+from pathlib import Path
+
+from aiidalab_qe.app.utils.plugin_manager import PluginManager
+
+# mock the content of the YAML file
+yaml_content = """
+    my-plugin:
+      title: My Test Plugin
+      description: A test plugin
+      author: John Doe
+      pip: my-plugin
+      status: beta
+    another-plugin:
+      title: Another Plugin
+      description: Another test plugin
+      author: Jane Doe
+      pip: another-plugin
+      status: experimental
+    """
+
+
+def test_plugin_manager_local_config_file():
+    """
+    Test that PluginManager loads the YAML file and builds the UI (Accordion) properly.
+    """
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        tmp_path = Path(tmp_dir)
+
+        config_file = tmp_path / "test_plugins.yaml"
+        config_file.write_text(yaml_content)
+
+        # Instantiate the manager
+        manager = PluginManager(config_source=str(config_file))
+        manager._build_ui()
+        assert len(manager.accordion.children) == 2, "Should build 2 accordion panels."
+
+        # Check that the titles are set correctly
+        assert "My Test Plugin" in manager.accordion.get_title(
+            0
+        ), "Check initial title for not-installed plugin"
+        assert "Another Plugin" in manager.accordion.get_title(
+            1
+        ), "Check second plugin title"
+
+
+def test_plugin_manager_default_config_file():
+    """
+    Test that PluginManager loads the YAML file from default source (GitHub repo).
+    """
+    manager = PluginManager()
+    manager._build_ui()
+    assert (
+        len(manager.accordion.children) > 0
+    ), "Should build at least one accordion panels."


Reviews:
user: login: edan-bainglass
body: Thanks @superstar54. I suggested a few changes. 

Also, a few issues with the available plugins list:

```
Bader charge analysis (aiida-bader) ☐ -> why not aiidalab-bader?
Phonons and IR/Raman (aiidalab-qe-vibroscopy) ☐ -> icon not working
Muon spectroscopy (aiidalab-qe-muon) ☐
Core-level spectroscopy (aiida-qe-xspec) ✅ -> again, why not aiidalab-...?
Wannier functions (aiidalab-qe-wannier90) ☐
Habbard parameter (aiidalab-qe-hp) ☐ -> Hubbard, not Habbard
```
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-06T10:16:44Z
user: login: edan-bainglass
body: Thanks @superstar54. This is good to go 👍
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T14:44:27Z

Review comments:
diff_hunk: @@ -99,12 +102,31 @@ def _render(self):
             "selected_index",
         )
 
+        self.install_new_plugin_button = LinkButton(
+            description="Open Plugin Store",
+            link="plugin_manager.ipynb",
+            icon="puzzle-piece",  # More intuitive icon
+            button_style="primary",  # Keeps it prominent
+            tooltip="Browse and install additional plugins from the Plugin Store",
+        )
+
         self.sub_steps = ipw.Accordion(
             children=[
                 ipw.VBox(
                     children=[
                         InAppGuide(identifier="properties-selection"),
-                        *self.property_children,
+                        *self.installed_property_children,
+                        ipw.HTML(
+                            value="""
+                            <p style="font-size:14px; line-height:1.6;">
+                                🔹 The following properties are currently unavailable.
+                                📦 To enable them, please visit the <b>Plugin Store</b> and install the required plugins.
+                            </p>
+                            """,
+                            layout=ipw.Layout(margin="10px 0px"),
+                        ),
path: src/aiidalab_qe/app/configuration/__init__.py
user: login: edan-bainglass
body: ```suggestion
                        ipw.HTML("<hr>"),
                        ipw.HTML(
                            value="""
                            <p style="font-size:14px; line-height:1.6;">
                                To enable the following properties, please visit the
                                <b>Plugin store</b> and install the required plugins.
                            </p>
                            """,
                            layout=ipw.Layout(margin="0 0 10px"),
                        ),
```
created_at: 2025-03-06T09:57:46Z
author_association: MEMBER
diff_hunk: @@ -99,12 +102,31 @@ def _render(self):
             "selected_index",
         )
 
+        self.install_new_plugin_button = LinkButton(
+            description="Open Plugin Store",
path: src/aiidalab_qe/app/configuration/__init__.py
user: login: edan-bainglass
body: ```suggestion
            description="Plugin store",
```
created_at: 2025-03-06T10:02:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,58 @@
+<div class="guide">
+    <h2>How to use this page</h2>
+    <p>
+        This page lets you manage the plugins of the AiiDAlab Quantum ESPRESSO app.
+        Below, you will find all available plugins from the
+        <a href="https://github.com/aiidalab/aiidalab-qe/blob/main/plugins.yaml" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            official AiiDAlab Quantum ESPRESSO plugin registry
+        </a>.
+    </p>
+    <p>
+        If you install a new plugin, you may need to <strong>refresh the calculation page</strong>
+        for the newly installed functionality to appear.
+    </p>
+    <p>
+        If you are developing a new plugin and would like to share it with the community,
+        <a href="https://aiidalab-qe.readthedocs.io/development/plugin_registry.html" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            click here
+        </a>
+        to learn how to register it in the official plugin registry.
+    </p>
+    <p>
+        You can install and uninstall plugins directly from this page.
+    </p>
+
+    <h3 style="margin-top: 20px;">Plugin Status Explanation</h3>
path: src/aiidalab_qe/app/static/templates/plugin_manager.jinja
user: login: edan-bainglass
body: ```suggestion
    <h3 style="margin-top: 20px;">Plugin status explanation</h3>
```
created_at: 2025-03-06T10:09:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,58 @@
+<div class="guide">
+    <h2>How to use this page</h2>
+    <p>
+        This page lets you manage the plugins of the AiiDAlab Quantum ESPRESSO app.
+        Below, you will find all available plugins from the
+        <a href="https://github.com/aiidalab/aiidalab-qe/blob/main/plugins.yaml" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            official AiiDAlab Quantum ESPRESSO plugin registry
+        </a>.
+    </p>
+    <p>
+        If you install a new plugin, you may need to <strong>refresh the calculation page</strong>
+        for the newly installed functionality to appear.
+    </p>
+    <p>
+        If you are developing a new plugin and would like to share it with the community,
+        <a href="https://aiidalab-qe.readthedocs.io/development/plugin_registry.html" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            click here
+        </a>
+        to learn how to register it in the official plugin registry.
+    </p>
+    <p>
+        You can install and uninstall plugins directly from this page.
+    </p>
+
+    <h3 style="margin-top: 20px;">Plugin Status Explanation</h3>
+    <p>Each plugin has a <strong>status badge</strong> indicating its development stage:</p>
+
+    <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
+        <thead>
+            <tr style="background-color: #f4f4f4;">
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Status</th>
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Meaning</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟠 <strong>Experimental</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is in an early stage of development and may be unstable. Expect rapid changes and potential issues.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟡 <strong>Beta</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is functional and being tested, but some features may still be refined.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🔵 <strong>Stable</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is feature-complete, has undergone significant testing, and is considered reliable for most use cases. Minor updates or bug fixes may still occur.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟢 <strong>Production</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is fully stable, widely tested, and officially recommended for all users. It follows strict maintenance and support policies.</td>
+            </tr>
+        </tbody>
+    </table>
+
+    <h3 style="margin-top: 20px;">Available Plugins</h3>
path: src/aiidalab_qe/app/static/templates/plugin_manager.jinja
user: login: edan-bainglass
body: ```suggestion
    <h3 style="margin-top: 20px;">Available plugins</h3>
```
created_at: 2025-03-06T10:12:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,58 @@
+<div class="guide">
+    <h2>How to use this page</h2>
+    <p>
+        This page lets you manage the plugins of the AiiDAlab Quantum ESPRESSO app.
+        Below, you will find all available plugins from the
+        <a href="https://github.com/aiidalab/aiidalab-qe/blob/main/plugins.yaml" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            official AiiDAlab Quantum ESPRESSO plugin registry
+        </a>.
+    </p>
+    <p>
+        If you install a new plugin, you may need to <strong>refresh the calculation page</strong>
+        for the newly installed functionality to appear.
+    </p>
+    <p>
+        If you are developing a new plugin and would like to share it with the community,
+        <a href="https://aiidalab-qe.readthedocs.io/development/plugin_registry.html" target="_blank"
+            style="background-color: #ffdd57; padding: 2px 5px; border-radius: 4px; font-weight: bold; text-decoration: none;">
+            click here
+        </a>
+        to learn how to register it in the official plugin registry.
+    </p>
+    <p>
+        You can install and uninstall plugins directly from this page.
+    </p>
+
+    <h3 style="margin-top: 20px;">Plugin Status Explanation</h3>
+    <p>Each plugin has a <strong>status badge</strong> indicating its development stage:</p>
+
+    <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
+        <thead>
+            <tr style="background-color: #f4f4f4;">
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Status</th>
+                <th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Meaning</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟠 <strong>Experimental</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is in an early stage of development and may be unstable. Expect rapid changes and potential issues.</td>
+            </tr>
+            <tr>
+                <td style="border: 1px solid #ddd; padding: 8px;">🟡 <strong>Beta</strong></td>
+                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is functional and being tested, but some features may still be refined.</td>
path: src/aiidalab_qe/app/static/templates/plugin_manager.jinja
user: login: edan-bainglass
body: ```suggestion
                <td style="border: 1px solid #ddd; padding: 8px;">The plugin is functional and being tested but some features may still be refined.</td>
```
created_at: 2025-03-06T10:13:09Z
author_association: MEMBER

PR comments:
user: login: superstar54
created_at: 2025-03-06T08:36:19Z
body: Hi @edan-bainglass , all tests passed, ready for review.
author_association: MEMBER
user: login: superstar54
created_at: 2025-03-06T13:56:23Z
body: @edan-bainglass Thanks for the review. I implemented the review suggestions.

> Phonons and IR/Raman (aiidalab-qe-vibroscopy) ☐ -> icon not working

I removed the checkbox `icon` if the plugin is not installed. 




author_association: MEMBER