aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--labbot/__main__.py21
-rw-r--r--labbot/addons/dashboard/__init__.py110
-rw-r--r--labbot/addons/dashboard/templates/addon_settings.html82
-rw-r--r--labbot/addons/dashboard/templates/base.html153
-rw-r--r--labbot/addons/dashboard/templates/index.html7
-rw-r--r--labbot/addons/dashboard/templates/log.html9
-rw-r--r--labbot/addons/dashboard/templates/settings.html24
-rw-r--r--labbot/addons/dashboard/templates/sidebar.html76
-rw-r--r--labbot/bot.py8
-rw-r--r--labbot/config.py4
-rw-r--r--requirements.txt4
11 files changed, 484 insertions, 14 deletions
diff --git a/labbot/__main__.py b/labbot/__main__.py
index 39d8842..fa76c0f 100644
--- a/labbot/__main__.py
+++ b/labbot/__main__.py
@@ -12,7 +12,8 @@ import labbot.logger
DEFAULT_ADDONS = [
"merge-label",
"approve-merge",
- "merge-stable"
+ "merge-stable",
+ "dashboard"
]
@click.group()
@@ -52,13 +53,12 @@ def config(name, **data):
conf.update(data)
labbot.config.write_instance_config(name, conf)
click.echo("configured")
- elif not print_config:
- click.echo("run with `--help` to show usage")
-
- if print_config:
- conf["access_token"] = "************"
- conf["secret"] = "******"
+ elif print_config:
+ conf["access_token"] = (round(len(conf["access_token"]) / 4) * 4) * "*"
+ conf["secret"] = (round(len(conf["secret"]) / 4) * 4) * "*"
click.echo(json.dumps(conf, indent=4))
+ else:
+ click.echo("run with `--help` to show usage")
else:
click.echo(f"{name} is not an instance")
pass
@@ -82,11 +82,14 @@ def run(name, port: str, debug: bool):
labbot.logger.init(logger_level)
+ access_token = conf.pop("access_token")
+ secret = conf.pop("secret", "")
+
instance = labbot.bot.Bot(
name=name,
config=conf,
- secret=conf["secret"],
- access_token=conf["access_token"]
+ access_token=access_token,
+ secret=secret,
)
instance.run(
diff --git a/labbot/addons/dashboard/__init__.py b/labbot/addons/dashboard/__init__.py
new file mode 100644
index 0000000..9e63cbc
--- /dev/null
+++ b/labbot/addons/dashboard/__init__.py
@@ -0,0 +1,110 @@
+"""
+a web dashboard for lab-bot
+"""
+import os
+import logging
+import json
+
+import aiohttp
+import jinja2
+import aiohttp_jinja2 # type: ignore
+
+from labbot import __version__ as labbot_version
+from labbot.config import Config
+
+log = logging.getLogger(__name__)
+
+class BufferHandler(logging.Handler):
+ def __init__(self, dashboard, lines=-1):
+ super().__init__(level=logging.DEBUG)
+ self.dashboard = dashboard
+ self.lines = lines
+
+ def emit(self, record):
+
+ try:
+ msg = self.format(record)
+ return self.dashboard.log_buffer.append(msg)
+ finally:
+ if self.lines > 0:
+ while len(self.dashboard.log_buffer) > self.lines:
+ self.dashboard.log_buffer.remove(0)
+
+class Dashboard:
+
+ def __init__(self, bot):
+ self.bot = bot
+ self.app = self.bot.instance.app
+ self.log_buffer = []
+
+ formatter = logging.Formatter(
+ "[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
+ )
+
+ buffer_handler = BufferHandler(self)
+ buffer_handler.setFormatter(formatter)
+
+ root_logger = logging.getLogger()
+ root_logger.addHandler(buffer_handler)
+ del root_logger
+
+ dashboard_dir = os.path.join(os.path.dirname(__file__), "templates")
+ aiohttp_jinja2.setup(
+ self.app,
+ context_processors=[self.processor],
+ loader=jinja2.FileSystemLoader(dashboard_dir))
+
+ self.pages = [
+ ["/", self.dashboard],
+ ["/log", self.log],
+ ["/settings", self.settings],
+ ["/settings/dashboard", self.addon_settings("dashboard")],
+ ]
+
+ for addon in self.bot.addons:
+ self.pages.append([f"/settings/{addon}", self.addon_settings(addon)])
+
+ for page in self.pages:
+ endpoint, func = page
+ self.app.router.add_get(endpoint, func)
+
+ @aiohttp_jinja2.template('index.html')
+ async def dashboard(self, request: aiohttp.web.Request) -> dict:
+ return {}
+
+ @aiohttp_jinja2.template('log.html')
+ async def log(self, request: aiohttp.web.Request) -> dict:
+ return {}
+
+ @aiohttp_jinja2.template('settings.html')
+ async def settings(self, request: aiohttp.web.Request) -> dict:
+ return {}
+
+ def addon_settings(self, addon):
+
+ @aiohttp_jinja2.template('addon_settings.html')
+ async def _settings(request: aiohttp.web.Request) -> dict:
+ c = Config(addon, self.bot.name)
+ return {
+ "addon_settings": c.settings
+ }
+
+ return _settings
+
+ async def processor(self, request) -> dict:
+ return {
+ "bot": {
+ "name": self.bot.name,
+ "version": labbot_version,
+ "addons": self.bot.addons,
+ "config": self.bot.config,
+
+ "log": "\n".join(self.log_buffer)
+ }
+ }
+
+ async def event_processor(self, *args, **kwargs):
+ self.event_counter += 1
+
+def setup(bot):
+ Dashboard(bot)
diff --git a/labbot/addons/dashboard/templates/addon_settings.html b/labbot/addons/dashboard/templates/addon_settings.html
new file mode 100644
index 0000000..853cd15
--- /dev/null
+++ b/labbot/addons/dashboard/templates/addon_settings.html
@@ -0,0 +1,82 @@
+{% extends "base.html" %}
+{% block content %}
+
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h1 class="h2">Addon Settings</h1>
+ </div>
+
+ <div>
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h4 class="h4">GLOBAL</h4>
+ </div>
+
+ <table class="table table-sm table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for k, v in addon_settings.GLOBAL.items() %}
+ <tr>
+ <td>{{k}}</td>
+ <td>{{v}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+
+ <div>
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h4 class="h4">GROUP</h4>
+ </div>
+
+ {% for group_id, setting in addon_settings.GROUP.items() %}
+ <h6>{{ group_id }}</h6>
+ <table class="table table-sm table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for k, v in setting.items() %}
+ <tr>
+ <td>{{k}}</td>
+ <td>{{v}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% endfor %}
+ </div>
+
+ <div>
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h4 class="h4">PROJECT</h4>
+ </div>
+
+ {% for project_id, setting in addon_settings.PROJECT.items() %}
+ <h6>{{ project_id }}</h6>
+ <table class="table table-sm table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for k, v in setting.items() %}
+ <tr>
+ <td>{{k}}</td>
+ <td>{{v}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% endfor %}
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/labbot/addons/dashboard/templates/base.html b/labbot/addons/dashboard/templates/base.html
new file mode 100644
index 0000000..03a5532
--- /dev/null
+++ b/labbot/addons/dashboard/templates/base.html
@@ -0,0 +1,153 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{{ bot.name }} - lab-bot {{ bot.version }}</title>
+
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
+
+
+ <!-- Custom styles for this template -->
+ <style type="text/css">
+ body {
+ font-size: .875rem;
+ }
+
+ .feather {
+ width: 16px;
+ height: 16px;
+ }
+
+ /*
+ * Sidebar
+ */
+
+ .sidebar {
+ position: fixed;
+ top: 0;
+ /* rtl:raw:
+ right: 0;
+ */
+ bottom: 0;
+ /* rtl:remove */
+ left: 0;
+ z-index: 100; /* Behind the navbar */
+ padding: 48px 0 0; /* Height of navbar */
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
+ }
+
+ @media (max-width: 767.98px) {
+ .sidebar {
+ top: 5rem;
+ }
+ }
+
+ .sidebar-sticky {
+ height: calc(100vh - 48px);
+ overflow-x: hidden;
+ overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
+ }
+
+ .sidebar .nav-link {
+ font-weight: 500;
+ color: #333;
+ }
+
+ .sidebar .nav-link .feather {
+ margin-right: 4px;
+ color: #727272;
+ }
+
+ .sidebar .nav-link:hover .feather {
+ color: inherit;
+ }
+
+ .sidebar-heading {
+ font-size: .75rem;
+ }
+
+ /*
+ * Navbar
+ */
+
+ .navbar-brand {
+ padding-top: .75rem;
+ padding-bottom: .75rem;
+ background-color: rgba(0, 0, 0, .25);
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
+ }
+
+ .navbar .navbar-toggler {
+ top: .25rem;
+ right: 1rem;
+ }
+
+ .navbar .form-control {
+ padding: .75rem 1rem;
+ }
+
+ .form-control-dark {
+ color: #fff;
+ background-color: rgba(255, 255, 255, .1);
+ border-color: rgba(255, 255, 255, .1);
+ }
+
+ .form-control-dark:focus {
+ border-color: transparent;
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
+ }
+
+ .log {
+ margin-top: 20px;
+ padding: 15px;
+ border: 2px solid;
+ background-color: black;
+ color: white;
+ overflow: scroll;
+ overflow-y: scroll;
+ max-height: 80vh;
+ }
+
+ .sidebar-bottom {
+ position: absolute;
+
+ bottom: 0;
+ }
+
+
+
+ </style>
+ </head>
+ <body>
+
+<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
+ <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="#">{{ bot.name }}</a>
+ <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="navbar-nav">
+ <div class="nav-item text-nowrap">
+ <a class="nav-link px-3" href="#">Sign out</a>
+ </div>
+ </div>
+</header>
+
+<div class="container-fluid">
+ <div class="row">
+ {% include "sidebar.html" %}
+
+ <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
+ {% block content %}{% endblock %}
+ </main>
+ </div>
+</div>
+
+
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
+ <script>
+ feather.replace();
+ </script>
+ </body>
+</html>
diff --git a/labbot/addons/dashboard/templates/index.html b/labbot/addons/dashboard/templates/index.html
new file mode 100644
index 0000000..78ebebf
--- /dev/null
+++ b/labbot/addons/dashboard/templates/index.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% block content %}
+
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h1 class="h2">Dashboard</h1>
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/labbot/addons/dashboard/templates/log.html b/labbot/addons/dashboard/templates/log.html
new file mode 100644
index 0000000..441bdd3
--- /dev/null
+++ b/labbot/addons/dashboard/templates/log.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+{% block content %}
+
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h1 class="h2">Log</h1>
+ </div>
+
+ <pre class="log">{{bot.log}}</pre>
+{% endblock %} \ No newline at end of file
diff --git a/labbot/addons/dashboard/templates/settings.html b/labbot/addons/dashboard/templates/settings.html
new file mode 100644
index 0000000..9bf03fb
--- /dev/null
+++ b/labbot/addons/dashboard/templates/settings.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+{% block content %}
+
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+ <h1 class="h2">Settings</h1>
+ </div>
+
+ <table class="table table-sm table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for k, v in bot.config.items() %}
+ <tr>
+ <td>{{k}}</td>
+ <td>{{v}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+{% endblock %} \ No newline at end of file
diff --git a/labbot/addons/dashboard/templates/sidebar.html b/labbot/addons/dashboard/templates/sidebar.html
new file mode 100644
index 0000000..f234f54
--- /dev/null
+++ b/labbot/addons/dashboard/templates/sidebar.html
@@ -0,0 +1,76 @@
+<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
+ <div class="position-sticky pt-3 sidebar-sticky">
+ <ul class="nav flex-column">
+ <li class="nav-item">
+ <a class="nav-link" aria-current="page" href="/">
+ <span data-feather="home" class="align-text-bottom"></span>
+ Dashboard
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" aria-current="page" href="/log">
+ <span data-feather="file-text" class="align-text-bottom"></span>
+ Log
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" aria-current="page" href="/settings">
+ <span data-feather="settings" class="align-text-bottom"></span>
+ Bot Settings
+ </a>
+ </li>
+ </ul>
+
+ <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
+ <span>Addon Settings</span>
+ </h6>
+ <ul class="nav flex-column mb-2">
+ {% for addon in bot.addons %}
+ <li class="nav-item">
+ <a class="nav-link" href="/settings/{{ addon }}">
+ <span data-feather="file-plus" class="align-text-bottom"></span>
+ {{ addon }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+
+ <div class="sidebar-bottom">
+ <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
+ <span>Health</span>
+ </h6>
+ <ul class="nav flex-column mb-2">
+ <li class="nav-item">
+ <a class="nav-link">
+ <span data-feather="heart" class="align-text-bottom"></span>
+ <span id="bot-health">Unknown</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</nav>
+
+<script>
+ function getHealth()
+ {
+ var href = window.location.href;
+ var osi = -2;
+ var nsi = -1;
+
+ while (nsi > osi)
+ {
+ osi = nsi;
+ nsi = href.indexOf("/", nsi+1);
+ }
+ href = href.slice(0, osi+1) + "health"
+
+ var xhttp = new XMLHttpRequest();
+ xhttp.onload = function() {
+ document.getElementById("bot-health").innerHTML = this.responseText;
+ }
+ xhttp.open("GET", href, true);
+ xhttp.send()
+ }
+ getHealth()
+</script> \ No newline at end of file
diff --git a/labbot/bot.py b/labbot/bot.py
index ef13978..72d01ca 100644
--- a/labbot/bot.py
+++ b/labbot/bot.py
@@ -13,9 +13,9 @@ class Bot:
def __init__(self, *args, **kwargs):
self.name = kwargs.pop("name", "lab-bot")
- self.access_token = kwargs.get("access_token")
- self.secret = kwargs.get("secret", "")
- self.config = kwargs.pop("config", labbot.config.read_instance_config(self.name))
+ self.access_token = kwargs.pop("access_token")
+ self.secret = kwargs.pop("secret")
+ self.config = kwargs.pop("config")
self.config_addons = self.config.get("addons", [])
self.addons = []
@@ -43,8 +43,6 @@ class Bot:
import_module(f"{addon}").setup(self)
log.info(f"Loaded {addon}")
self.addons.append(addon)
- except ModuleNotFoundError:
- log.error(f"No addon named `{addon}`")
except Exception as e:
log.exception(e)
diff --git a/labbot/config.py b/labbot/config.py
index 07c4d48..3574760 100644
--- a/labbot/config.py
+++ b/labbot/config.py
@@ -41,6 +41,10 @@ class Config:
# write the hardcoded config data ontop of the loaded data
self.settings["GLOBAL"].update(global_data)
+
+ repo = self.settings.pop("REPO")
+ if repo:
+ self.settings["PROJECT"] = repo
except (IOError, ValueError):
pass
diff --git a/requirements.txt b/requirements.txt
index 12b02db..8f10d20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,7 @@
gidgetlab[aiohttp]
appdirs
click
+
+# dashboard
+jinja2
+aiohttp_jinja2