docs: Major improvements in screenshots/layout (#1052)

This commit is contained in:
James Read 2026-06-19 17:01:45 +01:00 committed by GitHub
commit ad158c46dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
129 changed files with 2050 additions and 180 deletions

View File

@ -12,7 +12,7 @@ logLevel: "INFO"
# Actions are commands that are executed by OliveTin, and normally show up as
# buttons on the WebUI.
#
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
# Docs: https://docs.olivetin.app/action_buttons/create_your_first.html
actions:
# Lots of people use OliveTin to build web interfaces for their electronics
# projects. It's best to install OliveTin as a native package (eg, .deb), and

View File

@ -0,0 +1,31 @@
---
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Pump ON - 5m
id: pump_on_5m
icon: restart
shell: |
echo "Pump started"
sleep 300
triggers:
- Update Water Level
- title: Update Water Level
id: update_water_level
shell: echo "Water level 47%"
hidden: true
execOnStartup: true
execOnCron: "*/1 * * * *"
dashboards:
- title: Human in the Control Loop
contents:
- title: Water tank
type: fieldset
contents:
- type: stdout-most-recent-execution
title: update_water_level
- title: Pump ON - 5m

2
docs/modules/ROOT/images/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**/custom-webui
**/__pycache__

View File

@ -0,0 +1,163 @@
# Documentation screenshots
Use [repo-helper](https://github.com/jamesread/repo-common) (`repo-helper screenshot`) to keep Antora doc images up to date. Each documented UI feature gets its own folder under `docs/modules/ROOT/images/`.
Reference implementation: `args/suggestions/`.
## Folder layout
Create one folder per doc page (or logical screenshot group):
```
docs/modules/ROOT/images/<topic>/<name>/
├── screenshots.ini # batch capture config for repo-helper
├── config.yaml # minimal OliveTin config for this screenshot only
├── setup_<variant>.py # Selenium setup script(s); each defines run(driver)
├── Makefile # start OliveTin, capture, stop
├── .gitignore # runtime artifacts (see below)
└── *.png # output images (committed)
```
Wire images in the matching `.adoc` page:
```asciidoc
image::<topic>/<name>/my-screenshot.png[]
```
Paths are relative to `docs/modules/ROOT/images/`.
## Port and OliveTin instance
- Doc screenshots use a **dedicated port** (not 1337) so they do not clash with a dev server.
- All screenshot folders share port **11337**.
- Set the same port in `config.yaml` (`listenAddressSingleHTTPFrontend`) and `screenshots.ini` (`base_url`).
- Start OliveTin from `service/` so the webui is found:
```bash
cd service && ./OliveTin -configdir /path/to/screenshot/folder/
```
The Makefile handles start/wait/capture/stop.
## screenshots.ini
Each `[section]` with a `url` is one PNG. Section `name` (or section title) becomes the filename (`name.png`).
```ini
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 640
height = 480
post_script_sleep = 0.5
[my-screenshot]
url = .
name = my-screenshot
script = setup_my_screenshot.py
```
Notes:
- `--config` must point at the real `screenshots.ini` in the folder; relative paths (`script`, `dir`) resolve from the INI directory.
- `url = .` loads the dashboard at `base_url`.
- Override `width`, `height`, `script`, etc. per section when needed.
Capture:
```bash
cd docs/modules/ROOT/images/<topic>/<name>
make update-screenshots
# or, if OliveTin is already running on that port:
repo-helper screenshot --config screenshots.ini
```
## config.yaml
Keep configs **minimal**: only actions, dashboards, and settings required for the screenshot.
- Match YAML examples shown in the doc page.
- Disable noise: `checkForUpdates: false`, `showFooter: false`, `logLevel: "WARN"`.
- Set argument `type` explicitly to avoid startup warnings.
- Omit `icon` on actions unless the screenshot needs a specific glyph. OliveTin 3k applies a default action icon (`defaultIconForActions`, currently the neutral CLI glyph) when `icon` is not set.
Reuse integration-test patterns where possible (`integration-tests/tests/*/config.yaml`).
## Setup scripts (Python)
repo-helper loads each `--script` file and calls `run(driver)`. Scripts run **in isolation** (no imports from sibling modules unless you add `sys.path` yourself); prefer one self-contained file per variant.
UI setup should mirror integration tests (`integration-tests/lib/elements.js`):
1. Wait for `body[loaded-dashboard]` before clicking actions.
2. Click action buttons via `[title="Action Title"]` or `.action-button button`.
3. For argument forms, wait for `body[loaded-argument-form]`.
4. Use `#argument-popup`, input ids (`#container`), etc.
Example skeleton:
```python
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def run(driver):
WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")
)
driver.find_element(By.CSS_SELECTOR, '[title="My Action"]').click()
WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-argument-form")
)
# optional: frame the form, open menus, inject overlays — see args/suggestions/
time.sleep(0.2)
```
### Headless Chrome limitations
repo-helper uses headless Chrome only. Native browser UI (e.g. `<datalist>` dropdowns, date pickers) often **does not appear** in screenshots. When needed, use `driver.execute_script(...)` in the setup script to render a representative overlay after opening the real form. See `args/suggestions/setup_chrome.py` and `setup_firefox.py`.
## Makefile template
Each screenshot folder needs only a thin `Makefile` that sets `CONFIGDIR` and includes the shared rules:
```makefile
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk
```
Shared targets (defined in `docs/modules/ROOT/images/screenshots.mk`):
- `make start` — background OliveTin with this folder's `config.yaml`
- `make` or `make update-screenshots` — stop any instance on 11337, start, run `repo-helper screenshot --config screenshots.ini`, stop
- `make stop` — kill whatever is listening on port 11337
## .gitignore (per folder)
```
custom-webui/
__pycache__/
```
## Checklist for a new doc screenshot
1. Create `docs/modules/ROOT/images/<topic>/<name>/` with the files above.
2. Add `config.yaml` that reproduces the doc example in the UI.
3. Write `setup_*.py` to reach the desired UI state; test selectors against the Vue UI.
4. Add sections to `screenshots.ini`; output PNG names match what the `.adoc` will reference.
5. Update the `.adoc` page: `image::<topic>/<name>/<png>[]`.
6. Run `make update-screenshots` and commit PNGs plus config/scripts.
7. Remove obsolete PNGs from `images/` if paths moved.
## Prompt template (for agents)
Use or adapt this when asking to add or refresh doc screenshots:
> Update the documentation screenshot(s) for `<page.adoc>`.
>
> - Put everything in `docs/modules/ROOT/images/<topic>/<name>/`: `screenshots.ini`, `config.yaml`, setup script(s), `Makefile`, `.gitignore`, and output PNGs.
> - Follow `docs/modules/ROOT/images/SCREENSHOTS.md` and copy structure from `docs/modules/ROOT/images/args/suggestions/`.
> - Use a dedicated OliveTin port (not 1337); start from `service/` with `-configdir` pointing at the screenshot folder.
> - Setup scripts should wait for `loaded-dashboard` / `loaded-argument-form` like integration tests; reuse selectors from `integration-tests/lib/elements.js` where applicable.
> - Update `image::` paths in the `.adoc` page to match the new folder.
> - Run `make update-screenshots` and verify the PNGs before finishing.

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,12 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Say Hello
shell: echo "Hello World!"
icon: smile
onclick: execution-dialog

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 720
height = 480
post_script_sleep = 0.5
[hello-world]
url = .
name = hello-world
script = setup_hello.py

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Show the Say Hello action on the default dashboard."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def run(driver):
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, 30).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
WebDriverWait(driver, 30).until(
lambda d: d.find_element(By.CSS_SELECTOR, '[title="Say Hello"]').is_displayed()
)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,37 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actionGroups:
jobs:
maxConcurrent: 1
queueSize: 5
actions:
- title: Restart service
icon: restart
onclick: execution-dialog
shell: echo "Service restarted"
- title: Long task
shell: sleep 120
timeout: 300
groups: [jobs]
- title: Backup job
shell: sleep 120
timeout: 300
groups: [jobs]
dashboards:
- title: Action button layout
contents:
- title: Examples
type: fieldset
contents:
- title: Restart service
- title: Long task
- title: Backup job

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 980
height = 420
post_script_sleep = 0.5
[layout]
url = /dashboards/Action%20button%20layout
name = layout
script = setup_layout.py

View File

@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Show action buttons in idle, running, and queued states."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
_START_ACTION_JS = """
const done = arguments[arguments.length - 1];
const title = arguments[0];
function bindingIdForTitle(actionTitle) {
const button = document.querySelector('[title="' + actionTitle + '"]');
if (!button) {
throw new Error('Action button not found: ' + actionTitle);
}
return button.closest('.action-button').id.replace('actionButton-', '');
}
function uniqueTrackingId() {
if (window.isSecureContext && window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
return 'doc-screenshot-' + Date.now() + '-' + Math.random();
}
window.client.startAction({
bindingId: bindingIdForTitle(title),
arguments: [],
uniqueTrackingId: uniqueTrackingId(),
}).then(() => done(true)).catch((err) => done(String(err)));
"""
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _start_action(driver, title):
driver.execute_async_script(_START_ACTION_JS, title)
def _wait_for_layout_states(driver, timeout=20):
def ready(d):
try:
restart = d.find_element(By.CSS_SELECTOR, '[title="Restart service"]')
running = d.find_element(
By.CSS_SELECTOR,
'[title="Long task"]'
).find_element(By.XPATH, './ancestor::div[contains(@class, "action-button")]//span[contains(@class, "execution-indicator-running")]')
queued = d.find_element(
By.CSS_SELECTOR,
'[title="Backup job"]'
).find_element(By.XPATH, './ancestor::div[contains(@class, "action-button")]//span[contains(@class, "execution-indicator-queued")]')
onclick = d.find_element(
By.CSS_SELECTOR,
'[title="Restart service"] .navigate-on-start',
)
except Exception:
return False
return all(
element.is_displayed()
for element in (restart, running, queued, onclick)
)
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_start_action(driver, "Long task")
time.sleep(0.3)
_start_action(driver, "Backup job")
_wait_for_layout_states(driver)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,17 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Check dmesg logs
icon: logs
onclick: execution-dialog
shell: |
echo "[ 0.000000] Linux version 6.8.7-100.fc38.x86_64 (mock build) #1 SMP PREEMPT_DYNAMIC"
echo "[ 0.123456] Command line: BOOT_IMAGE=/vmlinuz root=UUID=..."
echo "[ 1.234567] systemd[1]: Started OliveTin documentation screenshot service."
echo "[ 1.456789] eth0: renamed from enp0s3"
echo "[ 2.012345] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready"

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 900
height = 620
post_script_sleep = 0.5
[execution-dialog]
url = .
name = executionDialog
script = setup_execution_dialog.py

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Open the execution-dialog view for Check dmesg logs."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _wait_for_logs_page(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: "/logs/" in d.current_url and not d.current_url.rstrip("/").endswith("/logs")
)
def _wait_for_execution_complete(driver, timeout=15):
def finished(d):
try:
status = d.find_element(By.CSS_SELECTOR, ".execution-dialog-status").text
except Exception:
return False
return "Still running" not in status and "Queued" not in status
WebDriverWait(driver, timeout).until(finished)
def run(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
driver.find_element(By.CSS_SELECTOR, '[title="Check dmesg logs"]').click()
_wait_for_logs_page(driver)
_wait_for_execution_complete(driver)
WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.CSS_SELECTOR, "#execution-results-popup .xterm-rows").text.strip() != ""
)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,11 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Slow action
icon: clock
shell: sleep 5

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 900
height = 420
post_script_sleep = 0.5
[timeout-logs]
url = .
name = timeoutLogs
script = setup_timeout_logs.py

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Show a timed-out action on the logs page."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _wait_for_timed_out_log(driver, timeout=20):
def has_timed_out_row(d):
try:
status = d.find_element(By.CSS_SELECTOR, ".logs-table .status-timeout").text
except Exception:
return False
return "Timed out" in status
WebDriverWait(driver, timeout).until(has_timed_out_row)
def run(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
driver.find_element(By.CSS_SELECTOR, '[title="Slow action"]').click()
# Default timeout is 3 seconds; the action sleeps for 5.
time.sleep(5)
driver.execute_script("window.location.href = '/logs'")
WebDriverWait(driver, 15).until(
lambda d: d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr")
)
_wait_for_timed_out_log(driver)
time.sleep(0.2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,16 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Print a message
shell: echo {{ message }}
arguments:
- name: message
description: The message you want to print out on the shell.
title: Your Message
default: Hello World
type: ascii_sentence

View File

@ -0,0 +1,21 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 800
height = 480
post_script_sleep = 0.5
[args1]
url = .
name = args1
script = setup_args1.py
[args2]
url = .
name = args2
script = setup_args2.py
[args3]
url = .
name = args3
script = setup_args3.py

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Prepare the dashboard action-button screenshot."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def run(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
time.sleep(0.2)

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Prepare the argument-form screenshot."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _open_form(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
action = driver.find_element(By.CSS_SELECTOR, '[title="Print a message"]')
action.click()
_wait_for_body_attr(driver, "loaded-argument-form")
def run(driver):
_open_form(driver)
driver.execute_script(
"""
const input = document.getElementById('message');
if (input && !input.value) {
input.value = 'Hello World';
}
"""
)
time.sleep(0.2)

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Prepare the execution-results screenshot."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _wait_for_logs_page(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: "/logs/" in d.current_url and not d.current_url.rstrip("/").endswith("/logs")
)
def _wait_for_execution_complete(driver, timeout=15):
def finished(d):
try:
status = d.find_element(By.CSS_SELECTOR, ".execution-dialog-status").text
except Exception:
return False
return "Still running" not in status and "Queued" not in status
WebDriverWait(driver, timeout).until(finished)
def _start_action_and_open_logs(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: d.find_element(By.CSS_SELECTOR, 'button[name="start"]').is_enabled()
)
driver.execute_async_script(
"""
const done = arguments[arguments.length - 1];
const bindingId = document.body.getAttribute('loaded-argument-form');
window.client.startAction({
bindingId: bindingId,
arguments: [{ name: 'message', value: 'Hello World' }],
uniqueTrackingId: 'doc-screenshot-' + Date.now(),
}).then((response) => {
window.location.href = '/logs/' + response.executionTrackingId;
done(true);
}).catch((err) => done('error: ' + err));
"""
)
def run(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
action = driver.find_element(By.CSS_SELECTOR, '[title="Print a message"]')
action.click()
_wait_for_body_attr(driver, "loaded-argument-form")
_start_action_and_open_logs(driver)
_wait_for_logs_page(driver)
_wait_for_execution_complete(driver)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,21 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Restart Docker Container
icon: restart
shell: "echo 'Restarting container: {{ container }}'"
arguments:
- name: container
title: Container name
type: ascii_identifier
suggestions:
plex:
graefik:
grafana:
wifi-controller: WiFi Controller
firewall-controller: Firewall Controller

View File

@ -0,0 +1,16 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 800
height = 480
post_script_sleep = 0.5
[arg-suggestions-chrome]
url = .
name = arg-suggestions-chrome
script = setup_chrome.py
[arg-suggestions-firefox]
url = .
name = arg-suggestions-firefox
script = setup_firefox.py

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Prepare the Chrome-style suggestions screenshot."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _open_form(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
action = driver.find_element(
By.CSS_SELECTOR, '[title="Restart Docker Container"]'
)
action.click()
_wait_for_body_attr(driver, "loaded-argument-form")
driver.execute_script(
"""
const form = document.getElementById('argument-popup');
if (form) {
form.style.margin = '2rem auto';
form.style.maxWidth = '520px';
}
"""
)
def run(driver):
_open_form(driver)
driver.execute_script(
"""
const input = document.getElementById('container');
input.focus();
input.value = '';
document.getElementById('doc-suggestions-overlay')?.remove();
const rect = input.getBoundingClientRect();
const menu = document.createElement('div');
menu.id = 'doc-suggestions-overlay';
menu.style.position = 'fixed';
menu.style.left = `${rect.left}px`;
menu.style.top = `${rect.bottom + 2}px`;
menu.style.width = `${rect.width}px`;
menu.style.background = '#fff';
menu.style.border = '1px solid #888';
menu.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.2)';
menu.style.font = '13px sans-serif';
menu.style.zIndex = '9999';
const items = [
['firewall-controller', 'Firewall Controller'],
['graefik', ''],
['grafana', ''],
['plex', ''],
['wifi-controller', 'WiFi Controller'],
];
for (const [value, label] of items) {
const row = document.createElement('div');
row.style.padding = '4px 8px';
row.style.lineHeight = '1.3';
const valueEl = document.createElement('div');
valueEl.textContent = value;
valueEl.style.fontWeight = label ? '600' : '400';
row.appendChild(valueEl);
if (label) {
const labelEl = document.createElement('div');
labelEl.textContent = label;
labelEl.style.color = '#666';
labelEl.style.fontSize = '12px';
row.appendChild(labelEl);
}
menu.appendChild(row);
}
document.body.appendChild(menu);
"""
)
time.sleep(0.2)

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Prepare the Firefox-style suggestions screenshot."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_body_attr(driver, attr, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
)
def _open_form(driver):
_wait_for_body_attr(driver, "loaded-dashboard")
action = driver.find_element(
By.CSS_SELECTOR, '[title="Restart Docker Container"]'
)
action.click()
_wait_for_body_attr(driver, "loaded-argument-form")
driver.execute_script(
"""
const form = document.getElementById('argument-popup');
if (form) {
form.style.margin = '2rem auto';
form.style.maxWidth = '520px';
}
"""
)
def run(driver):
_open_form(driver)
driver.execute_script(
"""
const input = document.getElementById('container');
input.focus();
input.value = '';
document.getElementById('doc-suggestions-overlay')?.remove();
const rect = input.getBoundingClientRect();
const menu = document.createElement('div');
menu.id = 'doc-suggestions-overlay';
menu.style.position = 'fixed';
menu.style.left = `${rect.left}px`;
menu.style.top = `${rect.bottom + 2}px`;
menu.style.width = `${rect.width}px`;
menu.style.background = '#fff';
menu.style.border = '1px solid #ccc';
menu.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.15)';
menu.style.font = '13px sans-serif';
menu.style.zIndex = '9999';
for (const label of [
'Firewall Controller',
'graefik',
'grafana',
'plex',
'WiFi Controller',
]) {
const row = document.createElement('div');
row.textContent = label;
row.style.padding = '4px 8px';
menu.appendChild(row);
}
document.body.appendChild(menu);
"""
)
time.sleep(0.2)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,53 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Ping All Servers
icon: ping
shell: echo "ping all..."
- title: Ping hypervisor1
icon: ping
shell: echo "ping hypervisor1"
- title: Ping hypervisor2
icon: ping
shell: echo "ping hypervisor2"
- title: '{{ server.name }} Wake on Lan'
shell: echo "wol {{ server.name }}"
entity: server
- title: '{{ server.name }} Power Off'
shell: echo "poweroff {{ server.name }}"
entity: server
entities:
- file: servers.yaml
name: server
dashboards:
- title: My Servers
contents:
- title: All Servers
type: fieldset
contents:
- title: Ping All Servers
- title: Hypervisors
contents:
- title: Ping hypervisor1
- title: Ping hypervisor2
- type: fieldset
entity: server
title: 'Server: {{ server.hostname }}'
contents:
- type: display
title: |
Hostname: <strong>{{ server.name }}</strong>
IP Address: <strong>{{ server.ip }}</strong>
- title: '{{ server.name }} Wake on Lan'
- title: '{{ server.name }} Power Off'

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 980
height = 720
post_script_sleep = 0.5
[preview]
url = /dashboards/My%20Servers
name = preview
script = setup_preview.py

View File

@ -0,0 +1,9 @@
- name: server1
hostname: server1.example.com
ip: 192.168.0.1
- name: server2
hostname: server2.example.com
ip: 192.168.0.2
- name: server3
hostname: server3.example.com
ip: 192.168.0.3

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Open the My Servers dashboard from dashboards/intro.adoc."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _wait_for_my_servers_dashboard(driver, timeout=30):
def ready(d):
try:
ping_all = d.find_element(By.CSS_SELECTOR, '[title="Ping All Servers"]')
hypervisors = d.find_element(
By.XPATH,
'//button[contains(@class, "directory-button")]//span[contains(@class, "title") and text()="Hypervisors"]',
)
server1 = d.find_element(By.CSS_SELECTOR, '[title="server1 Wake on Lan"]')
server3 = d.find_element(By.CSS_SELECTOR, '[title="server3 Power Off"]')
except Exception:
return False
return all(
element.is_displayed()
for element in (ping_all, hypervisors, server1, server3)
)
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_wait_for_my_servers_dashboard(driver)
time.sleep(0.2)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,31 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actionGroups:
backup:
maxConcurrent: 1
queueSize: 5
actions:
- title: Check disk space
icon: disk
shell: |
echo "Filesystem Size Used Avail Use% Mounted on"
echo "/dev/sda1 50G 12G 38G 24% /"
- title: Restart service
icon: restart
shell: echo "Service restarted successfully"
- title: Slow action
icon: clock
shell: sleep 5
- title: Slow backup
icon: backup
shell: sleep 30
groups: [ backup ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,24 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 900
height = 480
post_script_sleep = 0.5
[logs-list]
url = .
name = logsList
script = setup_logs_list.py
height = 460
[logs-calendar]
url = .
name = logsCalendar
script = setup_logs_calendar.py
height = 640
[logs-queue]
url = .
name = logsQueue
script = setup_logs_queue.py
height = 520

View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Open the logs calendar view with executions on the current month."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
_START_ACTIONS_JS = """
const done = arguments[arguments.length - 1];
const titles = arguments[0];
function bindingIdForTitle(title) {
const button = document.querySelector('[title="' + title + '"]');
if (!button) {
throw new Error('Action button not found: ' + title);
}
return button.closest('.action-button').id.replace('actionButton-', '');
}
function uniqueTrackingId() {
if (window.isSecureContext && window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
return 'doc-screenshot-' + Date.now() + '-' + Math.random();
}
function startByTitle(title) {
return window.client.startAction({
bindingId: bindingIdForTitle(title),
arguments: [],
uniqueTrackingId: uniqueTrackingId(),
});
}
Promise.all(titles.map(startByTitle)).then(() => done(true)).catch((err) => done(String(err)));
"""
def _wait_for_dashboard(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
def run(driver):
_wait_for_dashboard(driver)
driver.execute_async_script(
_START_ACTIONS_JS,
["Check disk space", "Restart service"],
)
time.sleep(2)
driver.execute_script("window.location.href = '/logs/calendar'")
WebDriverWait(driver, 15).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".calendar-event")) >= 2
)
driver.execute_script(
"""
const today = new Date();
const key = today.getFullYear() + '-'
+ String(today.getMonth() + 1).padStart(2, '0') + '-'
+ String(today.getDate()).padStart(2, '0');
const cell = document.querySelector('[data-calendar-date="' + key + '"]');
if (cell) {
cell.scrollIntoView({ block: 'center' });
}
"""
)
time.sleep(0.2)

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Seed log entries and open the logs list view."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
_START_ACTIONS_JS = """
const done = arguments[arguments.length - 1];
const titles = arguments[0];
function bindingIdForTitle(title) {
const button = document.querySelector('[title="' + title + '"]');
if (!button) {
throw new Error('Action button not found: ' + title);
}
return button.closest('.action-button').id.replace('actionButton-', '');
}
function uniqueTrackingId() {
if (window.isSecureContext && window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
return 'doc-screenshot-' + Date.now() + '-' + Math.random();
}
function startByTitle(title) {
return window.client.startAction({
bindingId: bindingIdForTitle(title),
arguments: [],
uniqueTrackingId: uniqueTrackingId(),
});
}
Promise.all(titles.map(startByTitle)).then(() => done(true)).catch((err) => done(String(err)));
"""
def _wait_for_dashboard(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
def _wait_for_logs_table(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr")) >= 3
)
def run(driver):
_wait_for_dashboard(driver)
driver.execute_async_script(
_START_ACTIONS_JS,
["Check disk space", "Restart service", "Slow action"],
)
# Slow action uses the default 3 second timeout while sleeping for 5.
time.sleep(5)
driver.execute_script("window.location.href = '/logs'")
_wait_for_logs_table(driver)
WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.CSS_SELECTOR, ".logs-table .status-timeout").text.strip() != ""
)
time.sleep(0.2)

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Show queued executions on the logs queue page."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
_QUEUE_ACTIONS_JS = """
const done = arguments[arguments.length - 1];
const title = arguments[0];
const count = arguments[1];
function bindingIdForTitle(actionTitle) {
const button = document.querySelector('[title="' + actionTitle + '"]');
if (!button) {
throw new Error('Action button not found: ' + actionTitle);
}
return button.closest('.action-button').id.replace('actionButton-', '');
}
function uniqueTrackingId() {
if (window.isSecureContext && window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
return 'doc-screenshot-' + Date.now() + '-' + Math.random();
}
async function queueActions() {
const bindingId = bindingIdForTitle(title);
for (let i = 0; i < count; i++) {
await window.client.startAction({
bindingId: bindingId,
arguments: [],
uniqueTrackingId: uniqueTrackingId(),
});
}
}
queueActions().then(() => done(true)).catch((err) => done(String(err)));
"""
def _wait_for_dashboard(driver, timeout=15):
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
def run(driver):
_wait_for_dashboard(driver)
driver.execute_async_script(_QUEUE_ACTIONS_JS, "Slow backup", 3)
time.sleep(1)
driver.execute_script("window.location.href = '/logs/queue'")
WebDriverWait(driver, 15).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".queue-action-group-section")) >= 1
)
WebDriverWait(driver, 15).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".queue-position")) >= 1
)
time.sleep(0.2)

View File

@ -0,0 +1,49 @@
.PHONY: update-screenshots start stop
ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../../..)
OLIVETIN ?= $(ROOT)/service/OliveTin
PORT := 11337
ifndef CONFIGDIR
$(error CONFIGDIR must be set before including screenshots.mk)
endif
.DEFAULT_GOAL := update-screenshots
start:
@set -e; \
if curl -sf "http://localhost:$(PORT)/" >/dev/null 2>&1; then \
echo "Port $(PORT) is already in use; run 'make stop' first"; \
exit 1; \
fi; \
cd "$(ROOT)/service" && "$(OLIVETIN)" -configdir "$(CONFIGDIR)" & \
pid=$$!; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
if curl -sf "http://localhost:$(PORT)/" >/dev/null; then \
exit 0; \
fi; \
if ! kill -0 $$pid 2>/dev/null; then \
echo "OliveTin exited before listening on port $(PORT)"; \
exit 1; \
fi; \
sleep 1; \
done; \
echo "Timed out waiting for OliveTin on port $(PORT)"; \
exit 1
stop:
@set +e; \
if command -v fuser >/dev/null 2>&1; then \
fuser -k $(PORT)/tcp 2>/dev/null; \
else \
for pid in $$(lsof -t -i :$(PORT) 2>/dev/null); do kill $$pid 2>/dev/null; done; \
fi; \
for i in 1 2 3 4 5; do \
curl -sf "http://localhost:$(PORT)/" >/dev/null || exit 0; \
sleep 1; \
done; \
exit 0
update-screenshots: stop start
cd "$(CONFIGDIR)" && repo-helper screenshot --config screenshots.ini
@$(MAKE) stop

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,34 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Start {{ container.Names }}
icon: box
shell: echo "start {{ container.Names }}"
entity: container
- title: Stop {{ container.Names }}
icon: box
shell: echo "stop {{ container.Names }}"
entity: container
entities:
- file: containers.json
name: container
dashboards:
- title: My Containers
contents:
- title: 'Container {{ container.Names }} ({{ container.Image }})'
entity: container
type: fieldset
contents:
- type: display
title: |
{{ container.RunningFor }} <br /><br /><strong>{{ container.State }}</strong>
- title: 'Start {{ container.Names }}'
- title: 'Stop {{ container.Names }}'

View File

@ -0,0 +1,2 @@
{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-28 22:33:35 +0000 GMT","ID":"fcf468e18a0e","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"minecraft","Networks":"bridge","Ports":"","RunningFor":"3 minutes ago","Size":"0B","State":"created","Status":"Created"}
{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-23 23:18:57 +0000 GMT","ID":"442dd6fe316a","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"brave_shirley","Networks":"bridge","Ports":"","RunningFor":"4 days ago","Size":"0B","State":"created","Status":"Created"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 980
height = 520
post_script_sleep = 0.5
[preview]
url = /dashboards/My%20Containers
name = preview
script = setup_preview.py

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Open the My Containers dashboard for the container control panel solution."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _wait_for_my_containers_dashboard(driver, timeout=30):
def ready(d):
required_titles = [
"Start minecraft",
"Stop minecraft",
"Start brave_shirley",
"Stop brave_shirley",
]
for title in required_titles:
try:
button = d.find_element(By.CSS_SELECTOR, f'[title="{title}"]')
except Exception:
return False
if not button.is_displayed():
return False
return True
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_wait_for_my_containers_dashboard(driver)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,33 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Pump ON - 5m
id: pump_on_5m
icon: restart
shell: |
echo "Pump started"
sleep 300
triggers:
- Update Water Level
- title: Update Water Level
id: update_water_level
shell: echo "Water level 47%"
hidden: true
execOnStartup: true
execOnCron: "*/1 * * * *"
dashboards:
- title: Human in the Control Loop
contents:
- title: Water tank
type: fieldset
contents:
- type: stdout-most-recent-execution
title: update_water_level
- title: Pump ON - 5m

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 900
height = 420
post_script_sleep = 0.5
[preview]
url = /dashboards/Human%20in%20the%20Control%20Loop
name = preview
script = setup_preview.py

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Open the Human in the Control Loop dashboard with water level output."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _wait_for_water_level(driver, timeout=30):
def ready(d):
try:
output = d.find_element(By.CSS_SELECTOR, ".mre-output").text
pump = d.find_element(By.CSS_SELECTOR, '[title="Pump ON - 5m"]')
except Exception:
return False
return "Water level 47%" in output and pump.is_displayed()
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_wait_for_water_level(driver)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,30 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: get pods
icon: <iconify-icon icon="pajamas:pod"></iconify-icon>
shell: |
echo "NAME READY STATUS RESTARTS AGE"
echo "olivetin-7f8b9c6d4-xk2mp 1/1 Running 0 3d"
echo "postgres-5d4f8b7c9-mn8pq 1/1 Running 0 12d"
echo "nginx-ingress-controller-2h9k 1/1 Running 0 45d"
- title: restart postgres deployment
icon: <iconify-icon icon="pajamas:clear-all"></iconify-icon>
shell: echo "deployment.apps/postgres restarted"
- title: evacuate node
icon: <iconify-icon icon="pajamas:rocket-launch"></iconify-icon>
shell: echo "node/{{ NodeName }} cordoned and drained"
arguments:
- name: NodeName
type: ascii_identifier
choices:
- value: node1
- value: node2
- value: node3

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 980
height = 380
post_script_sleep = 0.5
[preview]
url = .
name = preview
script = setup_preview.py

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Show the default Actions dashboard for the Kubernetes control panel."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
ACTION_TITLES = [
"get pods",
"restart postgres deployment",
"evacuate node",
]
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _wait_for_actions(driver, timeout=30):
def ready(d):
for title in ACTION_TITLES:
try:
button = d.find_element(By.CSS_SELECTOR, f'[title="{title}"]')
except Exception:
return False
if not button.is_displayed():
return False
return len(d.find_elements(By.CSS_SELECTOR, ".action-button button")) >= 3
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_wait_for_actions(driver)
time.sleep(0.2)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,33 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: Stop {{ systemd_unit.unit }}
shell: echo "stop {{ systemd_unit.unit }}"
icon: <iconify-icon icon="zondicons:hand-stop"></iconify-icon>
entity: systemd_unit
- title: Start {{ systemd_unit.unit }}
shell: echo "start {{ systemd_unit.unit }}"
icon: <iconify-icon icon="ic:round-directions-run"></iconify-icon>
entity: systemd_unit
entities:
- file: systemd_units.json
name: systemd_unit
dashboards:
- title: My Services
contents:
- title: '{{ systemd_unit.description }}'
type: fieldset
entity: systemd_unit
contents:
- title: 'Status: {{ systemd_unit.sub }}'
type: display
- title: Start {{ systemd_unit.unit }}
- title: Stop {{ systemd_unit.unit }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,11 @@
[DEFAULT]
base_url = http://localhost:11337/
dir = .
width = 980
height = 620
post_script_sleep = 0.5
[preview]
url = /dashboards/My%20Services
name = preview
script = setup_preview.py

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Open the My Services dashboard for the systemd control panel solution."""
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def _wait_for_dashboard(driver, timeout=30):
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return !!window.client")
)
WebDriverWait(driver, timeout).until(
lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
)
def _wait_for_my_services_dashboard(driver, timeout=30):
def ready(d):
required_titles = [
"Start boot.mount",
"Stop boot.mount",
"Start podman.service",
"Start upsilon-drone.service",
]
for title in required_titles:
try:
button = d.find_element(By.CSS_SELECTOR, f'[title="{title}"]')
except Exception:
return False
if not button.is_displayed():
return False
return True
WebDriverWait(driver, timeout).until(ready)
def run(driver):
_wait_for_dashboard(driver)
_wait_for_my_services_dashboard(driver)
time.sleep(0.2)

View File

@ -0,0 +1,4 @@
{"unit":"boot.mount","load":"loaded","active":"active","sub":"mounted","description":"/boot"}
{"unit":"podman.service","load":"loaded","active":"inactive","sub":"dead","description":"Podman API Service"}
{"unit":"upsilon-drone.service","load":"loaded","active":"active","sub":"running","description":"upsilon-drone"}
{"unit":"podman.socket","load":"loaded","active":"active","sub":"listening","description":"Podman API Socket"}

Some files were not shown because too many files have changed in this diff Show More