Prepare release 0.0.1: install script (uninstall/purge, service, validation), README (how to use, config), UI and effective config updates
@@ -1,7 +1,7 @@
|
||||
# dnsmasq-webui
|
||||
|
||||
<div align="center">
|
||||
<img src="src/DnsmasqWebUI/wwwroot/logo.png" alt="dnsmasq-webui logo" width="200">
|
||||
<img src="src/DnsmasqWebUI/wwwroot/logo.webp" alt="dnsmasq-webui logo" width="200">
|
||||
</div>
|
||||
|
||||
A self-hosted web UI for managing [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) configuration and hosts. It runs alongside your existing dnsmasq setup and edits the same config files; it does not replace dnsmasq.
|
||||
@@ -70,7 +70,7 @@ On systemd-based systems you can install a unit so the app runs as a service. Us
|
||||
sudo ./scripts/install.sh --system --service # System service (starts at boot)
|
||||
```
|
||||
|
||||
After install: configure the app (see [Configuration](#configuration)), then start the service: `systemctl --user start dnsmasq-webui` or `sudo systemctl start dnsmasq-webui`. User services run only when you’re logged in unless you enable linger: `loginctl enable-linger`.
|
||||
After install: configure the app (see [Configuration](#configuration)), then start the service: `systemctl --user start dnsmasq-webui` or `sudo systemctl start dnsmasq-webui`. User services run only when you’re logged in unless you enable linger: `loginctl enable-linger`. If you add `--service` later or switch from user to system service (or vice versa), the script removes the previous service type before installing the new one.
|
||||
|
||||
**Update to latest release:**
|
||||
|
||||
@@ -80,6 +80,15 @@ After install: configure the app (see [Configuration](#configuration)), then sta
|
||||
|
||||
Reinstalls the latest release into the default user directory (~/.local/share/dnsmasq-webui).
|
||||
|
||||
**Uninstall:**
|
||||
|
||||
```bash
|
||||
./scripts/install.sh --uninstall # Remove services and symlinks only (keeps install dir)
|
||||
./scripts/install.sh --uninstall --purge # Also remove default install dir (~/.local/share/dnsmasq-webui)
|
||||
sudo ./scripts/install.sh --uninstall --purge --system # Also remove /opt/dnsmasq-webui
|
||||
./scripts/install.sh --uninstall --purge --dir /path/to/dir # Purge a specific install dir
|
||||
```
|
||||
|
||||
**Custom directory or specific version:**
|
||||
|
||||
```bash
|
||||
@@ -203,7 +212,7 @@ All scripts live under `scripts/` and are intended for Linux (WSL may work but i
|
||||
|
||||
### scripts/install.sh
|
||||
|
||||
**Purpose:** Download and install dnsmasq-webui from a GitHub release for the current OS/arch (RID). Supports install, **update** (reinstall latest to default dir), and switch-release (re-run with same `--dir` and different `--version`). Default install is user-writable (~/.local/share/dnsmasq-webui) with an optional symlink so you can run `dnsmasq-webui` from the terminal; use `--system` for a system-wide install to /opt (requires sudo). Use `--service` to install a systemd unit (user service without sudo, or system service with `--system`); only supported on systemd-based systems.
|
||||
**Purpose:** Download and install dnsmasq-webui from a GitHub release for the current OS/arch (RID). Supports install, **update** (reinstall latest to default dir), switch-release (re-run with same `--dir` and different `--version`), and **uninstall** (remove services and symlinks; use `--purge` to also remove the install directory). Default install is user-writable (~/.local/share/dnsmasq-webui) with an optional symlink so you can run `dnsmasq-webui` from the terminal; use `--system` for a system-wide install to /opt (requires sudo). Use `--service` to install a systemd unit (user service without sudo, or system service with `--system`); only supported on systemd-based systems. When installing a service, the script removes the other type first (e.g. switching from user to system service cleans up the user unit).
|
||||
|
||||
**Usage:** `./scripts/install.sh [OPTIONS]`
|
||||
|
||||
@@ -217,9 +226,59 @@ All scripts live under `scripts/` and are intended for Linux (WSL may work but i
|
||||
| `--update` | Reinstall latest into the default user directory (~/.local/share/dnsmasq-webui). |
|
||||
| `--dir DIR` | Install into DIR instead of default. |
|
||||
| `--system` | Install to /opt/dnsmasq-webui and symlink /usr/local/bin/dnsmasq-webui. Requires root (run with sudo). |
|
||||
| `--service` | Install a systemd unit so the app runs as a service. With `--system`: system unit (requires root, starts at boot). Without: user unit (no sudo, starts at login). Only on systemd-based systems. |
|
||||
| `--service` | Install a systemd unit so the app runs as a service. With `--system`: system unit (requires root, starts at boot). Without: user unit (no sudo, starts at login). Removes the other type first if present. Only on systemd-based systems. |
|
||||
| `--uninstall` | Remove systemd units and symlinks (user and, if root, system). Does not remove the install directory. |
|
||||
| `--purge` | With `--uninstall` only: also remove the install directory. Use `--dir DIR` or `--system` to target a specific location. Errors if used without `--uninstall`. |
|
||||
| `-h`, `-?`, `--help` | Show help. |
|
||||
|
||||
Invalid combinations (script errors with a clear message): `--purge` without `--uninstall`; `--uninstall` with `--version`, `--update`, or `--service`; `--list` with install/uninstall options.
|
||||
|
||||
**How to use the script**
|
||||
|
||||
1. **Run one command to get a runnable binary for your OS/arch** (no repo or build choice needed).
|
||||
Script detects RID and default repo (or git origin), downloads the matching zip, extracts it, and creates a symlink when possible.
|
||||
**Commands:** `curl -sSL https://raw.githubusercontent.com/alexhopeoconnor/dnsmasq-webui/main/scripts/install.sh | sh` or `./scripts/install.sh`
|
||||
|
||||
2. **Install into your home directory by default** (no root).
|
||||
Default is `~/.local/share/dnsmasq-webui`; symlink in `~/.local/bin` if writable.
|
||||
**Command:** `./scripts/install.sh`
|
||||
|
||||
3. **Install to /opt and have `dnsmasq-webui` in PATH for all users.**
|
||||
Script requires root, then installs to `/opt/dnsmasq-webui` and symlinks `/usr/local/bin/dnsmasq-webui`.
|
||||
**Command:** `sudo ./scripts/install.sh --system`
|
||||
|
||||
4. **Install a systemd unit that starts at boot** so the UI runs as a service.
|
||||
Script checks root and systemd, installs and enables the system unit, and removes the invoking user's user unit so you do not have both.
|
||||
**Command:** `sudo ./scripts/install.sh --system --service`
|
||||
|
||||
5. **Install a user systemd unit (no sudo)** that starts when you log in.
|
||||
Script installs and enables the user unit and removes any existing user unit; if a system unit exists it tells you how to remove it.
|
||||
**Command:** `./scripts/install.sh --service`
|
||||
|
||||
6. **Upgrade your existing install to the latest release** without changing where it is installed.
|
||||
`--update` keeps your current target (default dir, `--dir`, or `--system`) and installs latest there.
|
||||
**Commands:** `./scripts/install.sh --update` or `sudo ./scripts/install.sh --update --system` or `./scripts/install.sh --update --dir /path`
|
||||
|
||||
7. **Switch from user service to system service (or the other way)** without manually removing the old unit.
|
||||
Installing one type removes the other for that scope (or tells you how to remove the system unit when installing user).
|
||||
**Commands:** `sudo ./scripts/install.sh --system --service` (user to system) or `./scripts/install.sh --service` (system to user; run `sudo ./scripts/install.sh --uninstall` first if you had a system unit).
|
||||
|
||||
8. **Uninstall cleanly:** remove services and symlinks, and optionally delete the install directory.
|
||||
`--uninstall` removes units and symlinks; `--purge` also removes one install dir, and you must say which (default, `--dir`, or `--system`).
|
||||
**Commands:** `./scripts/install.sh --uninstall`, `./scripts/install.sh --uninstall --purge`, `sudo ./scripts/install.sh --uninstall --purge --system`, or `./scripts/install.sh --uninstall --purge --dir /path`
|
||||
|
||||
9. **List available releases** to choose which version to install.
|
||||
Script lists release tags and dates; list-only mode cannot be combined with install/uninstall so you do not accidentally change the system.
|
||||
**Command:** `./scripts/install.sh --list`
|
||||
|
||||
10. **Install from a fork or a specific GitHub repo** instead of the default.
|
||||
You pass the repo; script uses it for the API. Without a clone or `--repo`/GITHUB_REPO it errors so you fix the command instead of getting a confusing API failure.
|
||||
**Command:** `./scripts/install.sh --repo owner/dnsmasq-webui` or `GITHUB_REPO=owner/dnsmasq-webui ./scripts/install.sh`
|
||||
|
||||
11. **Install a specific release tag** (e.g. an older or pinned version) instead of latest.
|
||||
Script fetches that tag's release and the asset for your RID. If no asset matches your OS/arch it errors and prints the asset names so you can pick another tag or report a missing build.
|
||||
**Command:** `./scripts/install.sh --version v1.0.0`
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
@@ -239,6 +298,11 @@ sudo ./scripts/install.sh --system
|
||||
./scripts/install.sh --service
|
||||
sudo ./scripts/install.sh --system --service
|
||||
|
||||
# Uninstall (services + symlinks only; add --purge to remove install dir)
|
||||
./scripts/install.sh --uninstall
|
||||
./scripts/install.sh --uninstall --purge
|
||||
sudo ./scripts/install.sh --uninstall --purge --system
|
||||
|
||||
# Install to custom dir
|
||||
./scripts/install.sh --dir /opt/dnsmasq-webui
|
||||
|
||||
@@ -249,6 +313,29 @@ sudo ./scripts/install.sh --system --service
|
||||
./scripts/install.sh --repo alexhopeoconnor/dnsmasq-webui --list
|
||||
```
|
||||
|
||||
**How the script works and avoids mistakes**
|
||||
|
||||
1. **Modes are exclusive**
|
||||
The script does one of: list releases, uninstall, or install (including update/switch-release). It errors if you mix modes, e.g. `--uninstall` with `--update` or `--service`, or `--list` with install/uninstall options. `--purge` is only valid with `--uninstall`; using `--purge` alone errors.
|
||||
|
||||
2. **Privilege checks**
|
||||
`--system` (install to /opt and system-wide symlink) and `--system --service` (system systemd unit) require root. The script checks `id -u` and exits with a clear message (“Run with sudo: sudo $0 --system”) instead of failing partway. Uninstall with `--purge --system` also requires root and is checked before removing anything.
|
||||
|
||||
3. **Service switching**
|
||||
When you install a systemd unit, the script removes the *other* type first: installing a system service stops/disables and removes the invoking user’s user unit (via `SUDO_USER`), and installing a user service leaves any system unit in place but prints a note on how to remove it. You never end up with both user and system units for the same user by accident.
|
||||
|
||||
4. **Update and target dir**
|
||||
`--update` means “reinstall latest.” If you also pass `--dir DIR` or `--system`, that target is used (e.g. `--update --dir /opt/foo` or `sudo ./install.sh --update --system`). So upgrade-in-place is predictable and you don’t overwrite a different install.
|
||||
|
||||
5. **Uninstall is explicit**
|
||||
`--uninstall` removes only systemd units and symlinks unless you add `--purge`. With `--purge`, you must say *which* install to remove: default user dir (no extra flags), `--dir DIR`, or `--system` for /opt. That avoids accidentally deleting the wrong directory.
|
||||
|
||||
6. **Repo and release**
|
||||
When not in a git clone, the script needs a repo (e.g. `REPO_DEFAULT` in the one-liner or `--repo` / `GITHUB_REPO`). It errors with a clear message instead of hitting the GitHub API with an empty repo. If no asset matches your OS/arch (RID), it prints the available asset names so you can pick a different release or report a missing build.
|
||||
|
||||
7. **Dependencies**
|
||||
The script requires `curl`, `jq`, and `unzip`. It checks for `jq` before calling the GitHub API and errors with an install hint. `--service` requires `systemctl` (systemd) and errors on non-systemd systems instead of writing a unit that won’t be used.
|
||||
|
||||
---
|
||||
|
||||
### scripts/publish-self-contained.sh
|
||||
|
||||
@@ -13,6 +13,8 @@ LIST=false
|
||||
INSTALL_DIR=""
|
||||
SYSTEM_INSTALL=false
|
||||
SERVICE=false
|
||||
UNINSTALL=false
|
||||
PURGE=false
|
||||
UPDATE=false
|
||||
|
||||
# Default install dir (user-writable, no sudo). Overridden by --dir or --system.
|
||||
@@ -44,8 +46,15 @@ usage() {
|
||||
echo "Service (systemd):"
|
||||
echo " --service Install a systemd unit so the app can run as a service (start on boot)."
|
||||
echo " With --system: installs system unit (requires root). Without: installs user unit (no sudo)."
|
||||
echo " If you already have the other type (user vs system), it is removed first."
|
||||
echo " Only supported on systemd-based systems; errors if systemd is not available."
|
||||
echo ""
|
||||
echo "Uninstall:"
|
||||
echo " --uninstall Remove symlinks and systemd units (user and, if root, system). Does not remove install directory."
|
||||
echo " --purge With --uninstall: also remove the install directory (use --dir or --system to specify which)."
|
||||
echo " E.g. $0 --uninstall --purge # remove default user dir"
|
||||
echo " sudo $0 --uninstall --purge --system # also remove /opt/dnsmasq-webui"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Install latest (clone: auto repo; curl: use REPO_DEFAULT or --repo)"
|
||||
echo " $0 --update # Reinstall latest to default dir (upgrade)"
|
||||
@@ -54,6 +63,9 @@ usage() {
|
||||
echo " sudo $0 --system # System-wide install to /opt, runnable as dnsmasq-webui"
|
||||
echo " $0 --service # Install + user systemd service (starts when you log in)"
|
||||
echo " sudo $0 --system --service # Install + system systemd service (starts at boot)"
|
||||
echo " $0 --uninstall # Remove services and symlinks only"
|
||||
echo " $0 --uninstall --purge # Remove services, symlinks, and default install dir"
|
||||
echo " sudo $0 --uninstall --purge --system # Also remove /opt/dnsmasq-webui"
|
||||
echo " $0 --list # List releases"
|
||||
echo ""
|
||||
echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary (or dnsmasq-webui if symlink created). If you used --service, enable/start with systemctl."
|
||||
@@ -172,6 +184,92 @@ find_asset_url() {
|
||||
' | head -n1
|
||||
}
|
||||
|
||||
# Remove user systemd unit (stop, disable, rm file). No root. Safe if not present.
|
||||
remove_user_service() {
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
systemctl --user stop dnsmasq-webui.service 2>/dev/null || true
|
||||
systemctl --user disable dnsmasq-webui.service 2>/dev/null || true
|
||||
rm -f "${HOME:-}/.config/systemd/user/dnsmasq-webui.service" 2>/dev/null || true
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Remove system systemd unit (stop, disable, rm file). Requires root. Safe if not present.
|
||||
remove_system_service() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
systemctl stop dnsmasq-webui.service 2>/dev/null || true
|
||||
systemctl disable dnsmasq-webui.service 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/dnsmasq-webui.service 2>/dev/null || true
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Remove user service for a specific user (by name). Call as root with SUDO_USER.
|
||||
# Used when installing system service so the invoking user doesn't keep a user unit.
|
||||
remove_user_service_for() {
|
||||
local u
|
||||
u="$1"
|
||||
[ -n "$u" ] || return 0
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
# Run as that user to stop/disable and remove their user unit
|
||||
su "$u" -c 'systemctl --user stop dnsmasq-webui.service 2>/dev/null; systemctl --user disable dnsmasq-webui.service 2>/dev/null; rm -f ~/.config/systemd/user/dnsmasq-webui.service; systemctl --user daemon-reload 2>/dev/null' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Uninstall: remove services, symlinks, optionally purge install dir(s).
|
||||
do_uninstall() {
|
||||
echo "Uninstalling dnsmasq-webui..."
|
||||
# Remove user service (current user)
|
||||
remove_user_service
|
||||
# Remove system service (if root)
|
||||
remove_system_service
|
||||
# Remove symlinks
|
||||
if [ -n "${HOME:-}" ] && [ -L "${HOME}/.local/bin/dnsmasq-webui" ]; then
|
||||
rm -f "${HOME}/.local/bin/dnsmasq-webui"
|
||||
echo "Removed symlink ~/.local/bin/dnsmasq-webui"
|
||||
fi
|
||||
if [ "$(id -u)" -eq 0 ] && [ -L /usr/local/bin/dnsmasq-webui ]; then
|
||||
rm -f /usr/local/bin/dnsmasq-webui
|
||||
echo "Removed symlink /usr/local/bin/dnsmasq-webui"
|
||||
fi
|
||||
if [ "$PURGE" = true ]; then
|
||||
if [ -n "$INSTALL_DIR" ]; then
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
rm -rf "$INSTALL_DIR"
|
||||
echo "Removed directory $INSTALL_DIR"
|
||||
else
|
||||
echo "Directory $INSTALL_DIR not found."
|
||||
fi
|
||||
elif [ "$SYSTEM_INSTALL" = true ] && [ "$(id -u)" -eq 0 ]; then
|
||||
if [ -d /opt/dnsmasq-webui ]; then
|
||||
rm -rf /opt/dnsmasq-webui
|
||||
echo "Removed directory /opt/dnsmasq-webui"
|
||||
else
|
||||
echo "Directory /opt/dnsmasq-webui not found."
|
||||
fi
|
||||
else
|
||||
local default_dir
|
||||
default_dir="$(default_install_dir)"
|
||||
if [ -d "$default_dir" ]; then
|
||||
rm -rf "$default_dir"
|
||||
echo "Removed directory $default_dir"
|
||||
else
|
||||
echo "Directory $default_dir not found."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Services and symlinks removed. To also remove the install directory, run with --purge (and --dir or --system if needed)."
|
||||
fi
|
||||
echo "Uninstall complete."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Main install: fetch release, find asset for RID, download, extract, optionally symlink.
|
||||
do_install() {
|
||||
check_jq
|
||||
@@ -250,6 +348,7 @@ do_install() {
|
||||
}
|
||||
|
||||
# Install systemd unit. Call only when SERVICE=true and systemctl exists.
|
||||
# Removes the other service type first (user vs system) so switching works cleanly.
|
||||
# $1 = INSTALL_DIR (where DnsmasqWebUI binary and appsettings.json live)
|
||||
install_systemd_unit() {
|
||||
local dir bin
|
||||
@@ -260,6 +359,11 @@ install_systemd_unit() {
|
||||
return 0
|
||||
fi
|
||||
if [ "$SYSTEM_INSTALL" = true ]; then
|
||||
# Installing system service: remove existing system unit (idempotent), then remove user unit for whoever ran sudo so they don't have both
|
||||
remove_system_service
|
||||
if [ -n "${SUDO_USER:-}" ]; then
|
||||
remove_user_service_for "$SUDO_USER"
|
||||
fi
|
||||
cat > /etc/systemd/system/dnsmasq-webui.service << EOF
|
||||
[Unit]
|
||||
Description=dnsmasq-webui - Web UI for dnsmasq
|
||||
@@ -284,6 +388,12 @@ EOF
|
||||
echo " sudo systemctl enable dnsmasq-webui # already enabled for boot"
|
||||
echo " sudo systemctl status dnsmasq-webui # check status"
|
||||
else
|
||||
# Installing user service: remove existing user unit (idempotent)
|
||||
remove_user_service
|
||||
# We cannot remove system service without root; remind if they might have had one
|
||||
if [ -f /etc/systemd/system/dnsmasq-webui.service ] 2>/dev/null; then
|
||||
echo "Note: A system-wide service also exists. To remove it and use only this user service: sudo $0 --uninstall (then re-run without --system --service)." >&2
|
||||
fi
|
||||
mkdir -p "${HOME:-}/.config/systemd/user"
|
||||
cat > "${HOME:-}/.config/systemd/user/dnsmasq-webui.service" << EOF
|
||||
[Unit]
|
||||
@@ -352,6 +462,14 @@ while [ $# -gt 0 ]; do
|
||||
SERVICE=true
|
||||
shift
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL=true
|
||||
shift
|
||||
;;
|
||||
--purge)
|
||||
PURGE=true
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Error: unknown option $1" >&2
|
||||
usage >&2
|
||||
@@ -365,12 +483,39 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate option combinations to prevent mistakes
|
||||
if [ "$PURGE" = true ] && [ "$UNINSTALL" != true ]; then
|
||||
echo "Error: --purge must be used with --uninstall. E.g. $0 --uninstall --purge" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$UNINSTALL" = true ]; then
|
||||
if [ "$UPDATE" = true ] || [ -n "$VERSION" ] || [ "$SERVICE" = true ]; then
|
||||
echo "Error: --uninstall cannot be combined with install/update options (--version, --update, --service). Run uninstall alone, then install if needed." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$LIST" = true ]; then
|
||||
echo "Error: --uninstall cannot be combined with --list." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$LIST" = true ]; then
|
||||
if [ "$UNINSTALL" = true ] || [ "$UPDATE" = true ] || [ "$SERVICE" = true ] || [ -n "$VERSION" ]; then
|
||||
echo "Error: --list lists releases and exits; do not combine with install/uninstall options." >&2
|
||||
exit 1
|
||||
fi
|
||||
list_releases
|
||||
fi
|
||||
|
||||
if [ "$UNINSTALL" = true ]; then
|
||||
if [ "$PURGE" = true ] && [ "$SYSTEM_INSTALL" = true ] && [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: --uninstall --purge --system requires root. Run with sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
do_uninstall
|
||||
fi
|
||||
|
||||
if [ "$UPDATE" = true ]; then
|
||||
INSTALL_DIR="$(default_install_dir)"
|
||||
[ -z "$INSTALL_DIR" ] && INSTALL_DIR="$(default_install_dir)"
|
||||
VERSION="latest"
|
||||
fi
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["DnsmasqWebUI.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" sizes="32x32" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="favicon-48.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="favicon-192.png" />
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="favicon-512.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||
<title>@_appTitle</title>
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
@* Reusable value-only display for boolean/flag options. Edit mode: checkbox (unless readonly); View: "Enabled" / "Disabled". *@
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
|
||||
{
|
||||
var isSet = Descriptor.GetValue() is bool b && b;
|
||||
<input type="checkbox" class="form-check-input" checked="@isSet" @onchange="OnCheckedChange" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@(Descriptor.GetValue() is bool isSet && isSet ? "Enabled" : "Disabled")
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
|
||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
[Parameter] public EventCallback<bool> CheckedChanged { get; set; }
|
||||
|
||||
private async Task OnCheckedChange(ChangeEventArgs e)
|
||||
{
|
||||
await CheckedChanged.InvokeAsync(e.Value is true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
@* Value-only display for the port option. Edit mode: number input (unless readonly); View: text with "(default DNS port)". *@
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
|
||||
{
|
||||
var port = Descriptor.GetValue() is int p ? p : 53;
|
||||
<input type="number" class="form-control form-control-sm" style="max-width: 6rem;" min="1" max="65535" value="@port" @oninput="OnPortInput" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@(Descriptor.GetValue() is int port ? $"{port} (default DNS port)" : "(not set)")
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
|
||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
[Parameter] public EventCallback<int?> PortChanged { get; set; }
|
||||
|
||||
private async Task OnPortInput(ChangeEventArgs e)
|
||||
{
|
||||
if (int.TryParse(e.Value?.ToString(), out var port) && port >= 1 && port <= 65535)
|
||||
await PortChanged.InvokeAsync(port);
|
||||
else
|
||||
await PortChanged.InvokeAsync(null);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,44 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Config
|
||||
@using DnsmasqWebUI.Services.Abstractions
|
||||
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
|
||||
|
||||
@if (Descriptor.IsMultiValue)
|
||||
@* Custom fragment renders only the value; we keep the row shell (label + value slot + badge) here for consistency. *@
|
||||
@if (_customFragment is { } fragment)
|
||||
{
|
||||
var labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
||||
<li class="ec-field" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
||||
<span class="ec-field-label" title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></span>
|
||||
<span class="ec-field-value">@fragment(Descriptor)</span>
|
||||
@if (Descriptor.GetSource()?.IsReadOnly == true)
|
||||
{
|
||||
<span class="badge bg-secondary ms-1">readonly</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
else if (Descriptor.IsMultiValue)
|
||||
{
|
||||
var items = Descriptor.GetItems();
|
||||
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" />
|
||||
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" LabelTooltip="@_labelTooltip" />
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = Descriptor.GetValue();
|
||||
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
|
||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" />
|
||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
|
||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
|
||||
private RenderFragment<EffectiveConfigFieldDescriptor>? _customFragment;
|
||||
private string? _labelTooltip;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_customFragment = RenderFragmentRegistry.GetDisplayFragment(Descriptor.SectionId, Descriptor.OptionName);
|
||||
_labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
<span class="d-inline-block me-2" title="@(Source?.GetReadOnlyTooltip())">
|
||||
<span class="ec-field-value-item" title="@(Source?.GetReadOnlyTooltip())">
|
||||
@Value
|
||||
@if (Source?.IsReadOnly == true)
|
||||
{
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
<li>
|
||||
<strong>@Label:</strong>
|
||||
<li class="ec-field">
|
||||
<span class="ec-field-label" title="@LabelTooltip"><strong>@Label:</strong></span>
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit)
|
||||
{
|
||||
<span class="ec-field-value ec-field-value-multi">
|
||||
<textarea class="form-control form-control-sm" rows="@(Math.Max(2, (Items?.Count ?? 0) + 1))" @oninput="OnLinesInput" placeholder="One value per line">@_editLines</textarea>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="ec-field-value ec-field-value-multi">
|
||||
@if (Items?.Count > 0)
|
||||
{
|
||||
@for (var i = 0; i < Items.Count; i++)
|
||||
<ol class="ec-field-value-list">
|
||||
@foreach (var item in Items!)
|
||||
{
|
||||
if (i > 0) { <span class="me-2">, </span> }
|
||||
var item = Items[i];
|
||||
<EffectiveConfigMultiValueItem Value="@item.Value" Source="@item.Source" />
|
||||
<li><EffectiveConfigMultiValueItem Value="@item.Value" Source="@item.Source" /></li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">(none)</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</li>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Label { get; set; } = "";
|
||||
[Parameter] public IReadOnlyList<ValueWithSource>? Items { get; set; }
|
||||
[Parameter] public EventCallback<IReadOnlyList<string>> ItemsChanged { get; set; }
|
||||
[Parameter] public string? LabelTooltip { get; set; }
|
||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
|
||||
private string _editLines = "";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Items != null && Items.Count > 0)
|
||||
_editLines = string.Join("\n", Items.Select(i => i.Value));
|
||||
else
|
||||
_editLines = "";
|
||||
}
|
||||
|
||||
private async Task OnLinesInput(ChangeEventArgs e)
|
||||
{
|
||||
var text = e.Value?.ToString() ?? "";
|
||||
_editLines = text;
|
||||
var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
|
||||
await ItemsChanged.InvokeAsync(lines);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
<li title="@(Source?.GetReadOnlyTooltip())">
|
||||
<strong>@Label:</strong> @DisplayValue
|
||||
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
|
||||
<span class="ec-field-label" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></span>
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Source?.IsReadOnly != true)
|
||||
{
|
||||
<span class="ec-field-value">
|
||||
<input type="text" class="form-control form-control-sm" value="@DisplayValue" @oninput="OnValueInput" />
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="ec-field-value">@DisplayValue</span>
|
||||
@if (Source?.IsReadOnly == true)
|
||||
{
|
||||
<span class="badge bg-secondary ms-1">readonly</span>
|
||||
}
|
||||
}
|
||||
</li>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Label { get; set; } = "";
|
||||
[Parameter] public string DisplayValue { get; set; } = "";
|
||||
[Parameter] public ConfigValueSource? Source { get; set; }
|
||||
/// <summary>Optional explainer shown on hover over the label (e.g. from DnsmasqOptionTooltips).</summary>
|
||||
[Parameter] public string? LabelTooltip { get; set; }
|
||||
/// <summary>When Edit and not readonly: raised when the user changes the value. Not yet wired to save.</summary>
|
||||
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
|
||||
private async Task OnValueInput(ChangeEventArgs e)
|
||||
{
|
||||
var value = e.Value?.ToString() ?? "";
|
||||
await DisplayValueChanged.InvokeAsync(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
var sectionDescriptors = descriptors.Where(d => d.SectionId == sectionId).ToList();
|
||||
if (sectionDescriptors.Count > 0)
|
||||
{
|
||||
<EffectiveConfigSectionPanel SectionId="@sectionId" Title="@title" Descriptors="@sectionDescriptors" IsOpen="@IsOpen(sectionId)" OnToggle="@OnSectionToggle" />
|
||||
<EffectiveConfigSectionPanel SectionId="@sectionId" Title="@title" Descriptors="@sectionDescriptors" IsOpen="@IsOpen(sectionId)" OnToggle="@OnSectionToggle" DisplayMode="@DisplayMode" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
@code {
|
||||
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
||||
/// <summary>View = read-only; Edit = editable (future). Cascaded to field displays.</summary>
|
||||
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
|
||||
private HashSet<string> _openPanels = new();
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Touch-friendly header: min 44px tap target (WCAG 2.5.5) */
|
||||
/* Touch-friendly header: min 44px tap target (WCAG 2.5.5), breathing room from edges */
|
||||
::deep .ec-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.5rem 0;
|
||||
padding: 0.625rem 1rem;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
@@ -75,9 +75,10 @@
|
||||
}
|
||||
|
||||
::deep .ec-section-body {
|
||||
padding: 0.25rem 0 0.5rem 0;
|
||||
padding-left: 0.5rem;
|
||||
padding: 0.25rem 1rem 0.5rem 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
|
||||
margin-left: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -93,8 +94,69 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Field row: label + value + optional badge. Same structure for custom and default displays. */
|
||||
::deep .ec-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
::deep .ec-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
::deep .ec-field-label {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
::deep .ec-field-value {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Multi-value: full width below label, indented ordered list */
|
||||
::deep .ec-field-value-multi {
|
||||
flex-basis: 100%;
|
||||
padding-left: 1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-list {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-list li {
|
||||
margin-bottom: 0.2rem;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-item {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
::deep .ec-field-value-sep {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
::deep .ec-section-header {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
::deep .ec-section-body {
|
||||
padding-left: 0.35rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
|
||||
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
|
||||
<CascadingValue Value="DisplayMode">
|
||||
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
|
||||
<button type="button" class="ec-section-header" @onclick="ToggleSection" aria-expanded="@IsOpen">
|
||||
<span class="ec-section-title fw-bold">@Title</span>
|
||||
<i class="bi @(IsOpen ? "bi-chevron-down" : "bi-chevron-right") ec-section-chevron" aria-hidden="true"></i>
|
||||
@@ -16,7 +17,8 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
[Parameter] public string SectionId { get; set; } = "";
|
||||
@@ -24,6 +26,8 @@
|
||||
[Parameter] public IReadOnlyList<EffectiveConfigFieldDescriptor> Descriptors { get; set; } = null!;
|
||||
[Parameter] public bool IsOpen { get; set; }
|
||||
[Parameter] public EventCallback<string> OnToggle { get; set; }
|
||||
/// <summary>Cascaded from EffectiveConfigSection. View = read-only; Edit = editable (future).</summary>
|
||||
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||
|
||||
private async Task ToggleSection() => await OnToggle.InvokeAsync(SectionId);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid d-flex align-items-center">
|
||||
<a class="navbar-brand d-flex align-items-center gap-2" href="" aria-label="@AppTitle home">
|
||||
<img src="logo.png" alt="" class="brand-logo" />
|
||||
<img src="logo.webp" alt="" class="brand-logo" />
|
||||
<span>@AppTitle</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Short explainer tooltips for dnsmasq option names. Shown on hover over the field label.
|
||||
/// One entry per effective-config key for consistency.
|
||||
/// </summary>
|
||||
public static class DnsmasqOptionTooltips
|
||||
{
|
||||
/// <summary>Display label used in EffectiveConfigFieldBuilder for server/local multi-value.</summary>
|
||||
public const string ServerLocalLabel = "server / local";
|
||||
|
||||
private static readonly FrozenDictionary<string, string> Tooltips = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
// --- Hosts ---
|
||||
[DnsmasqConfKeys.NoHosts] = "Do not read /etc/hosts; only use addn-hosts and DHCP/hosts.",
|
||||
[DnsmasqConfKeys.AddnHosts] = "Additional hosts files (one per line).",
|
||||
|
||||
// --- Resolver / DNS ---
|
||||
[DnsmasqConfKeys.ExpandHosts] = "Expand simple names in /etc/hosts with the local domain.",
|
||||
[DnsmasqConfKeys.BogusPriv] = "Do not forward reverse lookups for private IP ranges.",
|
||||
[DnsmasqConfKeys.StrictOrder] = "Query servers in the order specified; do not try the next on failure.",
|
||||
[DnsmasqConfKeys.NoResolv] = "Do not read /etc/resolv.conf; use only server= and resolv-file=.",
|
||||
[DnsmasqConfKeys.DomainNeeded] = "Never forward plain names (without a domain) to upstream.",
|
||||
[DnsmasqConfKeys.Port] = "Listen on this port for DNS queries (default 53).",
|
||||
[ServerLocalLabel] = "Upstream DNS servers (server=) and local-only domains (local=). Order is preserved.",
|
||||
[DnsmasqConfKeys.Address] = "Map a domain or hostname to an IP (e.g. for ad blocking or local names). Can repeat.",
|
||||
[DnsmasqConfKeys.ResolvFile] = "File(s) to read upstream server addresses from (e.g. from DHCP). Can repeat.",
|
||||
|
||||
// --- DHCP ---
|
||||
[DnsmasqConfKeys.DhcpAuthoritative] = "DHCP server is authoritative; required for DHCP to work.",
|
||||
[DnsmasqConfKeys.LeasefileRo] = "Do not create or truncate the DHCP lease file.",
|
||||
[DnsmasqConfKeys.DhcpLeasefile] = "Path to the DHCP lease file.",
|
||||
[DnsmasqConfKeys.DhcpLeaseMax] = "Maximum number of DHCP leases to hold.",
|
||||
[DnsmasqConfKeys.DhcpTtl] = "TTL for DHCP names in the DNS cache.",
|
||||
[DnsmasqConfKeys.DhcpRange] = "DHCP address range(s) and lease time. Can repeat.",
|
||||
[DnsmasqConfKeys.DhcpHost] = "DHCP host entries (reservations by MAC or client-id). Can repeat.",
|
||||
[DnsmasqConfKeys.DhcpOption] = "DHCP option lines sent to clients. Can repeat.",
|
||||
|
||||
// --- Cache ---
|
||||
[DnsmasqConfKeys.CacheSize] = "Maximum number of DNS cache entries (0 to disable).",
|
||||
[DnsmasqConfKeys.LocalTtl] = "TTL for entries from /etc/hosts and DHCP.",
|
||||
[DnsmasqConfKeys.NoNegcache] = "Do not cache negative (NXDOMAIN) responses.",
|
||||
[DnsmasqConfKeys.NegTtl] = "TTL for negative (cached miss) responses.",
|
||||
[DnsmasqConfKeys.MaxTtl] = "Maximum TTL for cached responses (cap on upstream TTL).",
|
||||
[DnsmasqConfKeys.MaxCacheTtl] = "Maximum TTL for positive cache entries.",
|
||||
[DnsmasqConfKeys.MinCacheTtl] = "Minimum TTL for positive cache entries.",
|
||||
|
||||
// --- Process & networking ---
|
||||
[DnsmasqConfKeys.NoPoll] = "Do not poll /etc/resolv.conf for changes.",
|
||||
[DnsmasqConfKeys.BindInterfaces] = "Only bind to interfaces in use; do not bind to wildcard.",
|
||||
[DnsmasqConfKeys.Interface] = "Only use these interfaces for DHCP/DNS. Can repeat.",
|
||||
[DnsmasqConfKeys.ListenAddress] = "Listen on these addresses for DNS/DHCP. Can repeat.",
|
||||
[DnsmasqConfKeys.ExceptInterface] = "Do not use these interfaces. Can repeat.",
|
||||
[DnsmasqConfKeys.PidFile] = "Write process ID to this file.",
|
||||
[DnsmasqConfKeys.User] = "Run as this user after startup (requires root).",
|
||||
[DnsmasqConfKeys.Group] = "Run as this group after startup (requires root).",
|
||||
[DnsmasqConfKeys.LogFacility] = "Syslog facility for dnsmasq logs (e.g. local0, daemon).",
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
/// <summary>Returns a short tooltip for the option, or null if none is defined.</summary>
|
||||
public static string? Get(string optionName)
|
||||
{
|
||||
return Tooltips.TryGetValue(optionName, out var tip) ? tip : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace DnsmasqWebUI.Models.EffectiveConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Display mode for effective-config fields. Cascaded from the parent so that
|
||||
/// OptionItem, MultiValueRow, and custom value displays can render view vs edit appropriately.
|
||||
/// </summary>
|
||||
public enum EffectiveConfigDisplayMode
|
||||
{
|
||||
/// <summary>Show values as read-only text.</summary>
|
||||
View,
|
||||
|
||||
/// <summary>Show editable controls (inputs, toggles). Not yet implemented; plumbing only.</summary>
|
||||
Edit,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Lookup for custom display fragments per effective-config field (section + option).
|
||||
/// When non-null, the fragment renders the value portion of the row; label and source badge stay shared.
|
||||
/// Registered as singleton via assembly scanning (<see cref="IApplicationSingleton"/>).
|
||||
/// </summary>
|
||||
public interface IEffectiveConfigRenderFragmentRegistry : IApplicationSingleton
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a fragment that renders the custom value UI for this field, or null to use default rendering.
|
||||
/// </summary>
|
||||
RenderFragment<EffectiveConfigFieldDescriptor>? GetDisplayFragment(string sectionId, string optionName);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Maps (sectionId, optionName) to custom effective-config value display component types and builds
|
||||
/// RenderFragments that render those components with the descriptor.
|
||||
/// </summary>
|
||||
public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragmentRegistry
|
||||
{
|
||||
private readonly Dictionary<(string SectionId, string OptionName), Type> _displayComponents = new();
|
||||
|
||||
public EffectiveConfigRenderFragmentRegistry()
|
||||
{
|
||||
// Port: single-field display (e.g. "53 (default DNS port)").
|
||||
_displayComponents[(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Port)] = typeof(PortValueDisplay);
|
||||
|
||||
// Boolean flags: shared display ("Enabled" / "Disabled") for all flag options.
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionHosts, DnsmasqConfKeys.NoHosts);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ExpandHosts);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.BogusPriv);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.StrictOrder);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.NoResolv);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.DomainNeeded);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.NoNegcache);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoPoll);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.BindInterfaces);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpAuthoritative);
|
||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.LeasefileRo);
|
||||
}
|
||||
|
||||
private void RegisterFlag(string sectionId, string optionName)
|
||||
{
|
||||
_displayComponents[(sectionId, optionName)] = typeof(FlagValueDisplay);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RenderFragment<EffectiveConfigFieldDescriptor>? GetDisplayFragment(string sectionId, string optionName)
|
||||
{
|
||||
if (!_displayComponents.TryGetValue((sectionId, optionName), out var componentType))
|
||||
return null;
|
||||
|
||||
return descriptor => builder =>
|
||||
{
|
||||
builder.OpenComponent(0, componentType);
|
||||
builder.AddAttribute(1, "Descriptor", descriptor);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 761 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 85 KiB |