279 lines
9.9 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|