Merge branch 'next' of github.com:OliveTin/OliveTin into next
22
config.yaml
|
|
@ -9,18 +9,10 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
|||
# Docs: https://docs.olivetin.app/advanced_configuration/logs.html
|
||||
logLevel: "INFO"
|
||||
|
||||
# Action groups share a concurrency limit across multiple actions. When the
|
||||
# limit is reached, additional requests are queued and run in order.
|
||||
# Docs: https://docs.olivetin.app/action_customization/concurrency.html#action-groups
|
||||
actionGroups:
|
||||
backup-jobs:
|
||||
maxConcurrent: 1
|
||||
icon: backup
|
||||
|
||||
# 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
|
||||
|
|
@ -266,6 +258,7 @@ actions:
|
|||
timeout: 300
|
||||
icon: logs
|
||||
onclick: execution-dialog
|
||||
groups: [ con2queue10 ]
|
||||
execOnCron:
|
||||
- "@hourly"
|
||||
|
||||
|
|
@ -310,6 +303,17 @@ entities:
|
|||
- file: entities/containers.json
|
||||
name: container
|
||||
|
||||
# Action groups share a concurrency limit across multiple actions. When the
|
||||
# limit is reached, additional requests are queued and run in order.
|
||||
# Docs: https://docs.olivetin.app/action_customization/concurrency.html#action-groups
|
||||
actionGroups:
|
||||
backup-jobs:
|
||||
maxConcurrent: 1
|
||||
icon: backup
|
||||
con2queue10:
|
||||
maxConcurrent: 2
|
||||
queueSize: 10
|
||||
|
||||
# Dashboards are a way of taking actions from the default "actions" view, and
|
||||
# organizing them into groups - either into folders, or fieldsets.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||