diff options
Diffstat (limited to 'desktop/modules/vm/VmPage.qml')
| -rw-r--r-- | desktop/modules/vm/VmPage.qml | 182 |
1 files changed, 181 insertions, 1 deletions
diff --git a/desktop/modules/vm/VmPage.qml b/desktop/modules/vm/VmPage.qml index 4f9e0aa..9c731f5 100644 --- a/desktop/modules/vm/VmPage.qml +++ b/desktop/modules/vm/VmPage.qml @@ -31,11 +31,13 @@ Column { // correct the moment the page appears. Virsh.sampling = true; Virsh.refreshList(); + Docker.sampling = true; + Docker.refreshList(); if (!page.selected && Virsh.names.length) page.selected = Virsh.names[0]; if (page.selected) Virsh.loadSnapshots(page.selected); } - Component.onDestruction: Virsh.sampling = false + Component.onDestruction: { Virsh.sampling = false; Docker.sampling = false; } onSelectedChanged: if (selected) Virsh.loadSnapshots(selected) @@ -170,6 +172,9 @@ Column { // Actions and snapshots, for the selected VM only. Loader { active: isSel + // An inactive Loader keeps its last height, and Column + // would still reserve it; hidden children take no space. + visible: active width: parent.width sourceComponent: detail property string vmName: modelData @@ -180,6 +185,181 @@ Column { } } + // --- containers -------------------------------------------------------- + // + // One card per compose project, shaped like a VM row: a dot, the + // aggregate stats, and the containers with their verbs when selected. + // Nothing here is destructive enough to confirm: no rm, no prune. + + property string selectedProject: "" + + function containerActions(s) { + if (s === "running") return [["stop", "Stop"], ["restart", "Restart"]]; + if (s === "paused") return [["unpause", "Resume"]]; + return [["start", "Start"]]; + } + + Text { + visible: Docker.projects.length > 0 + text: "Containers" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: Docker.projects + + Rectangle { + id: card + required property string modelData + readonly property var cs: Docker.groups[modelData] ?? [] + readonly property int up: cs.filter(c => c.state === "running").length + readonly property bool isSel: page.selectedProject === modelData + readonly property var st: cs.map(c => Docker.stats[c.id]).filter(s => s) + readonly property real cpu: st.reduce((a, s) => a + s.cpu, 0) + readonly property real mem: st.reduce((a, s) => a + Math.max(0, s.mem), 0) + readonly property real memTotal: st.length ? st[0].memTotal : -1 + + width: page.width + implicitHeight: cCol.implicitHeight + 24 + radius: 10 + color: isSel ? Qt.alpha(Theme.surface, 0.7) : Qt.alpha(Theme.surface, 0.35) + border.width: 1 + border.color: isSel ? Qt.alpha(Theme.accent, 0.5) : "transparent" + + MouseArea { + anchors.fill: parent + onClicked: page.selectedProject = isSel ? "" : modelData + } + + Column { + id: cCol + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 12 } + spacing: 10 + + Row { + spacing: 10 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 9; height: 9; radius: 5 + color: up === 0 ? Theme.overlay : up === cs.length ? Theme.green : Theme.yellow + } + Text { + text: modelData + font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } + color: Theme.text + } + Text { + anchors.verticalCenter: parent.verticalCenter + text: cs.length > 1 ? `${up}/${cs.length} running` : (cs[0]?.status ?? "") + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + } + + Flow { + visible: up > 0 + width: parent.width + spacing: 20 + + Stat { + label: "CPU" + value: st.length ? cpu.toFixed(0) + "%" : "—" + fraction: st.length ? cpu / 100 : -1 + } + Stat { + label: "RAM" + value: st.length ? page.fmtBytes(mem) : "—" + fraction: memTotal > 0 ? mem / memTotal : -1 + } + } + + Loader { + active: isSel + visible: active + width: parent.width + sourceComponent: Column { + spacing: 8 + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.08) } + + // Whole-project verbs, by container id, so no compose + // file is needed. Pointless for a group of one. + Flow { + visible: cs.length > 1 + width: parent.width + spacing: 8 + Button { + visible: up < cs.length + text: "Start all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "start") + } + Button { + visible: up > 0 + text: "Stop all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "stop") + } + Button { + visible: up > 0 + text: "Restart all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "restart") + } + } + + Repeater { + model: cs + + Column { + id: ctr + required property var modelData + readonly property var s: Docker.stats[modelData.id] + width: parent.width + spacing: 4 + + Row { + width: parent.width + spacing: 8 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 7; height: 7; radius: 4 + color: page.stateColor(modelData.state) + } + Text { + width: parent.width - 15 + elide: Text.ElideRight + text: modelData.name + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.text + } + } + + Row { + spacing: 10 + Text { + anchors.verticalCenter: parent.verticalCenter + text: modelData.status + + (s && modelData.state === "running" + ? ` ${s.cpu.toFixed(0)}% ${page.fmtBytes(s.mem)}` : "") + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.overlay + } + Repeater { + model: page.containerActions(modelData.state) + Button { + required property var modelData + text: modelData[1] + onClicked: Docker.act([ctr.modelData.id], ctr.modelData.name, modelData[0]) + } + } + } + } + } + } + } + } + } + } + Component { id: detail |
