diff options
-rw-r--r-- | labbot/__main__.py | 21 | ||||
-rw-r--r-- | labbot/addons/dashboard/__init__.py | 110 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/addon_settings.html | 82 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/base.html | 153 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/index.html | 7 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/log.html | 9 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/settings.html | 24 | ||||
-rw-r--r-- | labbot/addons/dashboard/templates/sidebar.html | 76 | ||||
-rw-r--r-- | labbot/bot.py | 8 | ||||
-rw-r--r-- | labbot/config.py | 4 | ||||
-rw-r--r-- | requirements.txt | 4 |
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 |