Pods panel could previously only list pods - there was no way to create
one, start/stop/restart it, or attach a container to it from the UI.
Adds a "New Pod" modal (name + port mappings), a per-pod lifecycle menu
(start/stop/restart/remove), and an optional "Pod" field on the Create
Container modal to join an existing pod's network namespace. Backend
verified live against the real podman socket (/pods/create, /pods/{name}/
restart, container "pod" field).
Also fixes three real bugs found via live testing:
- Podman.page used Menu="Podman" instead of Menu="Tasks:<rank>", so the
plugin never actually appeared in Unraid's top navigation (traced through
PageBuilder.php/DefaultPageLayout.php/Navigation/Main.php - only pages
registered under "Tasks" become top-level tabs).
- app.js's shared context-menu component mis-mapped every item positioned
after a 'separator' entry to the wrong DOM element (an off-by-one against
menu.children, which includes the separator <div>s) - so "Remove", which
always sits after a separator, silently did nothing when clicked. Fixed
by indexing into querySelectorAll('button') instead.
- That same menu was positioned via "position: absolute" math that assumed
a viewport-relative containing block, but Unraid's own page wrapper
(webGui/styles/default-base.css's ".content") sets position:relative,
so the menu rendered far from its anchor button. Switched to
"position: fixed" with viewport-relative coordinates.
Incidentally, pods add a hidden "infra" container that was leaking into
the plain Containers list with no working lifecycle of its own (always
"running", so its own Remove was permanently disabled) - now filtered out
via libpod's IsInfra flag. And every action-buttons table cell used
"display: flex" directly on the <td>, which browsers can size
inconsistently row to row - moved onto an inner wrapper div instead, and
bumped .podman-btn-icon's touch target size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
277 lines
13 KiB
Plaintext
277 lines
13 KiB
Plaintext
Menu="Tasks:66"
|
|
Type="xmenu"
|
|
Tabs="false"
|
|
Title="Podman"
|
|
Icon="podman"
|
|
---
|
|
<?php
|
|
/**
|
|
* Podman.page — Unraid Dynamix-style GUI page for unraid-podman.
|
|
*
|
|
* This file only renders the page shell: the header, the sub-navigation
|
|
* for the ten sections, and one (mostly empty) container per panel. Every
|
|
* panel fills itself in via AJAX after load — see javascript/app.js's
|
|
* router and each javascript/<panel>.js module — so this file has no
|
|
* business logic of its own and does not talk to PodmanClient directly.
|
|
* See docs/ARCHITECTURE.md section 18 for the overall WebUI design and
|
|
* webui/mockups/prototype.html for the approved visual reference this
|
|
* page's markup mirrors (same structure, same CSS classes, real data
|
|
* instead of static samples).
|
|
*/
|
|
|
|
/**
|
|
* Cache-busts every static asset with its own on-disk mtime. Unraid's
|
|
* webserver sends no explicit no-cache headers for /plugins/ static
|
|
* files, so without this, browsers can keep serving a stale app.js/
|
|
* podman.css for a long time after a plugin update — verified live: a
|
|
* bugfix to app.js's CSRF handling silently kept failing in a real
|
|
* browser after redeploy until this was added, even though the deployed
|
|
* file on disk was byte-for-byte correct.
|
|
*/
|
|
function podman_asset_version(string $relPath): string
|
|
{
|
|
$full = __DIR__ . $relPath;
|
|
return is_file($full) ? (string) filemtime($full) : '0';
|
|
}
|
|
?>
|
|
<link rel="stylesheet" type="text/css" href="/plugins/podman/styles/podman.css?v=<?=podman_asset_version('/styles/podman.css')?>">
|
|
|
|
<div class="podman-plugin">
|
|
|
|
<header class="podman-pagehead">
|
|
<div class="titlewrap">
|
|
<div class="icon" aria-hidden="true">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 2l8 4.5v9L12 20l-8-4.5v-9L12 2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 11l8-4.5M12 11v9M12 11L4 6.5" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
|
|
</div>
|
|
<div>
|
|
<h1>Podman</h1>
|
|
<div class="meta" id="podman-header-meta">Connecting…</div>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex; gap:8px;">
|
|
<button class="podman-btn" id="podman-refresh-all">↻ Refresh</button>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="podman-subnav">
|
|
<button class="active" data-panel="dashboard">Dashboard</button>
|
|
<button data-panel="containers">Containers</button>
|
|
<button data-panel="templates">Templates</button>
|
|
<button data-panel="pods">Pods</button>
|
|
<button data-panel="images">Images</button>
|
|
<button data-panel="volumes">Volumes</button>
|
|
<button data-panel="networks">Networks</button>
|
|
<button data-panel="logs">Logs</button>
|
|
<button data-panel="terminal">Terminal</button>
|
|
<button data-panel="compose">Compose</button>
|
|
<button data-panel="settings">Settings</button>
|
|
</nav>
|
|
|
|
<main class="podman-main">
|
|
|
|
<!-- ============================= DASHBOARD ============================= -->
|
|
<section class="podman-panel active" id="podman-panel-dashboard">
|
|
<div class="podman-grid podman-stat-grid">
|
|
<div class="podman-stat"><div class="label">Running</div><div class="value tnum"><span id="stat-running">—</span> <small id="stat-running-total"></small></div></div>
|
|
<div class="podman-stat"><div class="label">Pods</div><div class="value tnum" id="stat-pods">—</div></div>
|
|
<div class="podman-stat"><div class="label">Images</div><div class="value tnum" id="stat-images">—</div></div>
|
|
<div class="podman-stat"><div class="label">Volumes</div><div class="value tnum" id="stat-volumes">—</div></div>
|
|
<div class="podman-stat"><div class="label">Networks</div><div class="value tnum" id="stat-networks">—</div></div>
|
|
<div class="podman-stat"><div class="label">Images on Disk</div><div class="value tnum" id="stat-images-size">—</div></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= CONTAINERS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-containers">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" id="containers-search" type="text" placeholder="Search containers by name or image…">
|
|
<div class="filterset" id="containers-filterset" style="display:flex; gap:4px; background:var(--surface-2); padding:3px; border-radius:8px;">
|
|
<button class="active" data-filter="all" id="containers-count-all">All</button>
|
|
<button data-filter="running" id="containers-count-running">Running</button>
|
|
<button data-filter="stopped" id="containers-count-stopped">Stopped</button>
|
|
</div>
|
|
<button class="podman-btn podman-btn-primary" id="containers-create-btn">+ New Container</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Status</th><th>Name</th><th>Image</th><th>CPU/Mem</th><th>Ports</th><th>Uptime</th><th></th></tr></thead>
|
|
<tbody id="containers-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= TEMPLATES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-templates"></section>
|
|
|
|
<!-- ============================= PODS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-pods"></section>
|
|
|
|
<!-- ============================= IMAGES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-images">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" type="text" placeholder="Search images…" disabled title="Client-side filtering not yet wired up for Images">
|
|
<button class="podman-btn" id="images-pull-btn">⬇ Pull Image</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Repository</th><th>Tag</th><th>Image ID</th><th>Size</th><th>Created</th><th>Used By</th><th></th></tr></thead>
|
|
<tbody id="images-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= VOLUMES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-volumes">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<div style="flex:1;"></div>
|
|
<button class="podman-btn podman-btn-primary" id="volumes-create-btn">+ New Volume</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Name</th><th>Driver</th><th>Mountpoint</th><th>Used By</th><th></th></tr></thead>
|
|
<tbody id="volumes-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="podman-empty-note" style="border-top:1px solid var(--border); text-align:left; padding:12px 18px;">
|
|
App data mapped from Unraid shares (e.g. <span class="mono">/mnt/user/appdata/…</span>) is managed as bind mounts per-container and does not appear here.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= NETWORKS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-networks">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<div style="flex:1;"></div>
|
|
<button class="podman-btn podman-btn-primary" id="networks-create-btn">+ New Network</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Name</th><th>Driver</th><th>Subnet</th><th>Gateway</th><th>Containers</th><th></th></tr></thead>
|
|
<tbody id="networks-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= LOGS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-logs">
|
|
<div class="podman-card">
|
|
<div class="podman-logs-layout">
|
|
<div class="podman-logs-side" id="logs-sidebar"></div>
|
|
<div>
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" id="logs-filter" type="text" placeholder="Filter log output…" style="max-width:280px;">
|
|
<span id="logs-follow-toggle" style="display:flex; gap:4px; background:var(--surface-2); padding:3px; border-radius:8px;">
|
|
<button class="active" data-follow="true">Follow</button>
|
|
<button data-follow="false">Paused</button>
|
|
</span>
|
|
</div>
|
|
<div class="podman-log-pane" id="log-pane" role="log"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= TERMINAL ============================= -->
|
|
<section class="podman-panel" id="podman-panel-terminal">
|
|
<div class="podman-card">
|
|
<div class="podman-card-pad">
|
|
<div style="display:flex; gap:8px; align-items:center; margin-bottom:12px; font-size:12.5px; color:var(--text-dim);">
|
|
Exec into: <select id="term-container-select"></select>
|
|
</div>
|
|
<div class="podman-term" id="term-output"></div>
|
|
<input class="podman-term-input" id="term-input" type="text" placeholder="Type a command and press Enter… (one-shot exec — see Compose panel note on API scope)" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= COMPOSE ============================= -->
|
|
<section class="podman-panel" id="podman-panel-compose">
|
|
<div class="podman-card">
|
|
<div class="podman-compose-layout">
|
|
<div class="podman-compose-side" id="compose-sidebar"></div>
|
|
<div>
|
|
<div class="podman-toolbar">
|
|
<strong id="compose-title" style="flex:1;">—</strong>
|
|
<button class="podman-btn" id="compose-action-pull">⬇ Pull</button>
|
|
<button class="podman-btn" id="compose-action-down">■ Down</button>
|
|
<button class="podman-btn podman-btn-primary" id="compose-action-up">▶ Up</button>
|
|
</div>
|
|
<pre class="podman-yaml" id="compose-yaml"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= SETTINGS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-settings">
|
|
<div class="podman-grid">
|
|
<div class="podman-card">
|
|
<div class="podman-card-head"><h2>Storage</h2></div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-storage-path">Storage path</label>
|
|
<div>
|
|
<input type="text" id="settings-storage-path" class="mono">
|
|
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-storage-size">podman.img size</label>
|
|
<div><input type="number" id="settings-storage-size" style="max-width:100px;"> <span style="font-size:12px;color:var(--text-dim);">GB</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="podman-card">
|
|
<div class="podman-card-head"><h2>Autostart & Lifecycle</h2></div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-enabled">Start podman on array start</label>
|
|
<div><input type="checkbox" id="settings-enabled"></div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-stop-timeout">Container stop timeout</label>
|
|
<div><input type="number" id="settings-stop-timeout" style="max-width:100px;"> <span style="font-size:12px;color:var(--text-dim);">seconds</span></div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label>Autostart order</label>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>#</th><th>Container</th><th></th></tr></thead>
|
|
<tbody id="autostart-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label></label>
|
|
<div><button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="podman-card">
|
|
<div class="podman-card-head"><h2>Installed Packages</h2></div>
|
|
<div class="podman-field-row">
|
|
<label>Versions</label>
|
|
<div class="hint mono" id="settings-package-versions" style="max-width:none;">—</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<?php
|
|
foreach ([
|
|
'app', 'dashboard', 'containers', 'templates', 'pods', 'images', 'volumes',
|
|
'networks', 'logs', 'terminal', 'compose', 'settings',
|
|
] as $podmanJsModule) {
|
|
$podmanJsPath = "/javascript/{$podmanJsModule}.js";
|
|
echo '<script src="/plugins/podman' . $podmanJsPath . '?v=' . podman_asset_version($podmanJsPath) . '"></script>' . "\n";
|
|
}
|
|
?>
|