nova-shell/shell/applets/SystemdMachineSection.qml

279 lines
9.9 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import "../services" as S
import NovaStats as NS
// One section of the systemd applet: a header with title, aggregate counts,
// state chip; an auto-expanded list of failed units (hidden when empty); a
// lazy-loaded, collapsed-by-default list of running units.
Column {
id: root
property color accentColor
// SSH target string (for remote hosts/their containers); "" for local-side entries.
property string hostTarget: ""
property string machineName: ""
property string title: ""
property string marker: ""
property string systemState: "unknown"
property int runningCount: 0
property int totalCount: 0
property var failedUnits: []
property var runningUnits: []
property string errorKind: ""
property string errorReason: ""
property int lastSeen: 0
property var containers: []
property int depth: 0
signal contentResized
onHeightChanged: root.contentResized()
width: parent?.width ?? 0
property bool _runningExpanded: false
readonly property int _failedCount: (failedUnits ?? []).length
// Header
Item {
width: root.width
height: 32
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _hdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _hdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: _stateChip.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: root.title + (root.marker !== "" ? " " + root.marker : "")
color: NS.ThemeService.base05
font.pixelSize: NS.ThemeService.fontSize
font.family: NS.ThemeService.fontFamily
elide: Text.ElideRight
}
// Aggregate counts: "n running, m/total failed" or "n running" if no failures.
Text {
id: _counts
anchors.right: _stateChip.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: {
if (root.totalCount === 0)
return "";
const r = root.runningCount + " running";
if (root._failedCount > 0)
return r + ", " + root._failedCount + "/" + root.totalCount + " failed";
return r;
}
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
Rectangle {
id: _stateChip
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: root.systemState !== "unknown"
color: {
const st = root.systemState;
if (root.errorKind === "permanent")
return NS.ThemeService.base08;
if (root.errorKind === "transient")
return NS.ThemeService.base04;
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
if (st === "pending")
return NS.ThemeService.base04;
if (st === "unreachable")
return NS.ThemeService.base04;
return NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _stateLbl.width + 8
height: 14
Text {
id: _stateLbl
anchors.centerIn: parent
text: root.systemState
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
}
// Error / last-seen line for remote machines. Hidden when there's no
// error and the machine is reachable (lastSeen > 0 with no errorKind).
Item {
visible: root.errorKind !== "" || (root.lastSeen > 0 && root.systemState === "unreachable")
width: root.width
height: visible ? 22 : 0
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: {
const ago = root.lastSeen > 0 ? " (last seen " + _agoString(root.lastSeen) + ")" : "";
if (root.errorReason !== "")
return root.errorReason + ago;
return ago.replace(/^\s+/, "");
}
color: root.errorKind === "permanent" ? NS.ThemeService.base08 : NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
elide: Text.ElideRight
}
}
function _agoString(unix) {
const now = Math.floor(Date.now() / 1000);
const d = Math.max(0, now - unix);
if (d < 60)
return d + "s ago";
if (d < 3600)
return Math.floor(d / 60) + "m ago";
if (d < 86400)
return Math.floor(d / 3600) + "h ago";
return Math.floor(d / 86400) + "d ago";
}
// Failed units (auto-expanded; entire block hidden when there are none).
Repeater {
model: root._failedCount > 0 ? root.failedUnits : []
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description ?? ""
subState: modelData.subState ?? ""
scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName
accentColor: root.accentColor
}
}
// Running units toggle row (only meaningful when there are running units to show).
Item {
visible: (root.runningUnits ?? []).length > 0
width: root.width
height: 26
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _runHdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _runHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.verticalCenter: parent.verticalCenter
text: "running units (" + (root.runningUnits ?? []).length + ")"
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._runningExpanded ? "" : ""
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.iconFontFamily
}
TapHandler {
onTapped: root._runningExpanded = !root._runningExpanded
}
}
// Lazy-loaded running units list. Repeater materializes rows only when the
// model is non-empty, so feeding `[]` while collapsed avoids per-row cost.
Repeater {
model: root._runningExpanded ? root.runningUnits : []
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description ?? ""
subState: modelData.subState ?? ""
scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName
accentColor: root.accentColor
}
}
// Nested containers running on this machine. Indented to convey hierarchy.
// QML disallows direct recursive component use, so we load the same .qml
// by source path through a Loader to break the static graph.
Repeater {
model: root.containers ?? []
delegate: Item {
id: _childWrap
required property var modelData
width: root.width
height: _childLoader.height + 4
Loader {
id: _childLoader
anchors.left: parent.left
anchors.leftMargin: 16
width: parent.width - 16
source: "SystemdMachineSection.qml"
onLoaded: {
item.accentColor = root.accentColor;
item.hostTarget = root.hostTarget;
item.machineName = _childWrap.modelData.name;
item.title = _childWrap.modelData.name;
item.marker = _childWrap.modelData.marker ?? "";
item.systemState = _childWrap.modelData.systemState ?? "unknown";
item.runningCount = _childWrap.modelData.runningCount ?? 0;
item.totalCount = _childWrap.modelData.totalCount ?? 0;
item.failedUnits = _childWrap.modelData.failedUnits ?? [];
item.runningUnits = _childWrap.modelData.runningUnits ?? [];
item.errorKind = _childWrap.modelData.errorKind ?? "";
item.errorReason = _childWrap.modelData.errorReason ?? "";
item.lastSeen = _childWrap.modelData.lastSeen ?? 0;
item.containers = _childWrap.modelData.containers ?? [];
item.depth = root.depth + 1;
item.contentResized.connect(root.contentResized);
}
}
}
}
}