docs: Major improvements in screenshots/layout (#1052)
|
|
@ -12,7 +12,7 @@ logLevel: "INFO"
|
||||||
# Actions are commands that are executed by OliveTin, and normally show up as
|
# Actions are commands that are executed by OliveTin, and normally show up as
|
||||||
# buttons on the WebUI.
|
# 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:
|
actions:
|
||||||
# Lots of people use OliveTin to build web interfaces for their electronics
|
# 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
|
# projects. It's best to install OliveTin as a native package (eg, .deb), and
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/custom-webui
|
||||||
|
**/__pycache__
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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"
|
||||||
|
After Width: | Height: | Size: 47 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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'
|
||||||
|
After Width: | Height: | Size: 64 KiB |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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 ]
|
||||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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 }}'
|
||||||
|
|
@ -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"}
|
||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
custom-webui/
|
||||||
|
__pycache__/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
include ../../screenshots.mk
|
||||||
|
|
@ -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 }}
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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"}
|
||||||