feat(docker): group containers by compose/swarm + enrich widget (milestone 77 #942)
Container list now sub-groups each host's containers by compose project (or swarm service) with a small subheading, preserving the running-first order; hosts with no such labels render flat as before. The dashboard status widget links each container to its detail page and surfaces enriched state inline — health dot (healthy/unhealthy), restart count, and last non-zero exit code for stopped containers. Completes the #942 UI surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -175,6 +175,23 @@ async def rows():
|
|||||||
else:
|
else:
|
||||||
g["stopped"] += 1
|
g["stopped"] += 1
|
||||||
|
|
||||||
|
# Sub-group each host's containers by compose project (or swarm service),
|
||||||
|
# preserving the running-first ordering. Containers with neither label fall
|
||||||
|
# into an unlabelled bucket rendered flat, so plain hosts look unchanged.
|
||||||
|
for g in groups.values():
|
||||||
|
subs: dict[str, dict] = {}
|
||||||
|
order: list[str] = []
|
||||||
|
for item in g["containers"]:
|
||||||
|
c = item["container"]
|
||||||
|
label = c.compose_project or c.service_name or None
|
||||||
|
key = label or ""
|
||||||
|
if key not in subs:
|
||||||
|
subs[key] = {"label": label, "containers": []}
|
||||||
|
order.append(key)
|
||||||
|
subs[key]["containers"].append(item)
|
||||||
|
g["subgroups"] = [subs[k] for k in order]
|
||||||
|
g["grouped"] = any(s["label"] for s in g["subgroups"])
|
||||||
|
|
||||||
host_groups = sorted(groups.values(), key=lambda g: g["host_name"].lower())
|
host_groups = sorted(groups.values(), key=lambda g: g["host_name"].lower())
|
||||||
running = sum(g["running"] for g in host_groups)
|
running = sum(g["running"] for g in host_groups)
|
||||||
total = len(containers)
|
total = len(containers)
|
||||||
|
|||||||
@@ -47,7 +47,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in g.containers %}
|
{% for sub in g.subgroups %}
|
||||||
|
{% if g.grouped and sub.label %}
|
||||||
|
<tr><td colspan="7" style="padding:0.4rem 0.75rem 0.2rem;font-size:0.72rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;">{{ sub.label }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for item in sub.containers %}
|
||||||
{% set c = item.container %}
|
{% set c = item.container %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -105,6 +109,7 @@
|
|||||||
<td>{{ item.sparkline_mem | safe }}</td>
|
<td>{{ item.sparkline_mem | safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,9 +24,12 @@
|
|||||||
{% for c in g.containers %}
|
{% for c in g.containers %}
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.82rem;padding:0.15rem 0;">
|
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.82rem;padding:0.15rem 0;">
|
||||||
<span class="dot {% if c.status == 'running' %}dot-up{% elif c.status == 'paused' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
<span class="dot {% if c.status == 'running' %}dot-up{% elif c.status == 'paused' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
||||||
<span style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
<a href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:inherit;text-decoration:none;">
|
||||||
{{ c.name }}
|
{{ c.name }}{% if c.health == 'unhealthy' %}<span title="unhealthy" style="color:var(--red);"> ●</span>{% elif c.health == 'healthy' %}<span title="healthy" style="color:var(--green);"> ●</span>{% endif %}{% if c.restart_count %}<span title="restarts" style="color:var(--orange);font-size:0.7rem;"> ⟳{{ c.restart_count }}</span>{% endif %}
|
||||||
</span>
|
</a>
|
||||||
|
{% if c.status != 'running' and c.exit_code is not none and c.exit_code != 0 %}
|
||||||
|
<span title="last exit code" style="font-size:0.7rem;color:var(--red);flex-shrink:0;">exit {{ c.exit_code }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if c.cpu_pct is not none %}
|
{% if c.cpu_pct is not none %}
|
||||||
<span style="font-size:0.72rem;color:var(--text-muted);font-family:ui-monospace,monospace;flex-shrink:0;">
|
<span style="font-size:0.72rem;color:var(--text-muted);font-family:ui-monospace,monospace;flex-shrink:0;">
|
||||||
{{ "%.1f" | format(c.cpu_pct) }}%
|
{{ "%.1f" | format(c.cpu_pct) }}%
|
||||||
|
|||||||
Reference in New Issue
Block a user