Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -16,11 +16,18 @@ import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
objectName: "containers"
|
||||
|
||||
title: "Containers"
|
||||
lede: "Local services you run for development — what they expose, and what they cost."
|
||||
|
||||
// The log reader replaces the page while it is open; SettingsPage owns the
|
||||
// swap so this file is one page rather than two stacked on a bare Item.
|
||||
drilledIn: Containers.logTarget !== ""
|
||||
|
||||
// "images", "volumes", or empty. Removal never happens on a first press.
|
||||
property string confirmingPrune: ""
|
||||
|
||||
@@ -58,371 +65,269 @@ Item {
|
||||
|
||||
// ── the list ────────────────────────────────────────────────────────────
|
||||
|
||||
SettingsPage {
|
||||
anchors.fill: parent
|
||||
visible: Containers.logTarget === ""
|
||||
TextRow {
|
||||
visible: Containers.lastError !== ""
|
||||
label: "That did not work"
|
||||
detail: Containers.lastError
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
title: "Containers"
|
||||
lede: "Local services you run for development — what they expose, and what they cost."
|
||||
TextRow {
|
||||
visible: Containers.scanned && !Containers.available
|
||||
label: "podman is not available"
|
||||
detail: "Nothing here can be shown until podman is installed."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.lastError !== ""
|
||||
label: "That did not work"
|
||||
detail: Containers.lastError
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
// ── finding: published to the network ───────────────────────────────
|
||||
|
||||
TextRow {
|
||||
visible: Containers.scanned && !Containers.available
|
||||
label: "podman is not available"
|
||||
detail: "Nothing here can be shown until podman is installed."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
// ── finding: published to the network ───────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
visible: Containers.reachable.length > 0
|
||||
title: Containers.reachable.length === 1
|
||||
? "A container is published to your whole network"
|
||||
: "Containers are published to your whole network"
|
||||
subtitle: "These bind every interface, so any machine on your network can connect. "
|
||||
+ "For a development database, loopback is almost always what you want. "
|
||||
+ "Binding adds an address to the compose file and changes nothing else."
|
||||
|
||||
Repeater {
|
||||
model: Containers.reachable
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
label: String(modelData.container ?? "")
|
||||
detail: "Port " + modelData.hostPort + "/" + String(modelData.protocol ?? "tcp")
|
||||
+ " · " + String(modelData.image ?? "")
|
||||
+ (modelData.configFile
|
||||
? "\n" + Containers.shorten(String(modelData.configFile))
|
||||
: "\nNo compose file is recorded for this container, so it cannot be bound from here.")
|
||||
controlWidth: 250
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Open compose file"
|
||||
visible: String(modelData.configFile ?? "") !== ""
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.openFile(String(modelData.configFile))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Bind to localhost"
|
||||
tone: "accent"
|
||||
// Needs both halves of the label to find the entry in
|
||||
// the compose file; the Supabase CLI records neither.
|
||||
visible: String(modelData.configFile ?? "") !== ""
|
||||
&& String(modelData.service ?? "") !== ""
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.bindLocal(
|
||||
String(modelData.project), String(modelData.service))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.reachable.length > 0
|
||||
label: "After binding"
|
||||
detail: "The change takes effect the next time the stack comes up, because a "
|
||||
+ "published port is fixed when the container is created."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
// ── finding: disk nothing references ────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
visible: Containers.unusedImages.length > 0 || Containers.unusedVolumes.length > 0
|
||||
title: Containers.formatBytes(Containers.reclaimable) + " nothing is using"
|
||||
subtitle: "Images and volumes no container references. Removing them frees the space; "
|
||||
+ "anything still needed downloads again on next use."
|
||||
|
||||
Repeater {
|
||||
model: Containers.unusedImages.slice(0, 5)
|
||||
|
||||
delegate: TextRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: ""
|
||||
value: Containers.formatBytes(Number(modelData.size ?? 0))
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.unusedImages.length > 5
|
||||
label: "and " + (Containers.unusedImages.length - 5) + " more"
|
||||
detail: ""
|
||||
value: ""
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
label: root.confirmingPrune === "images"
|
||||
? "Remove " + Containers.unusedImages.length + " images?"
|
||||
: "Unused images"
|
||||
detail: root.confirmingPrune === "images"
|
||||
? "Each is removed by name. Nothing that a container references is touched."
|
||||
: Containers.unusedImages.length + " images, "
|
||||
+ Containers.formatBytes(Number(Containers.disk.imagesReclaimable ?? 0))
|
||||
visible: Containers.unusedImages.length > 0
|
||||
controlWidth: 230
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: root.confirmingPrune === "images" ? "Keep them" : "Remove…"
|
||||
enabled: !Containers.busy
|
||||
onClicked: root.confirmingPrune =
|
||||
root.confirmingPrune === "images" ? "" : "images"
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
visible: root.confirmingPrune === "images"
|
||||
text: "Remove them"
|
||||
tone: "danger"
|
||||
enabled: !Containers.busy
|
||||
onClicked: {
|
||||
root.confirmingPrune = "";
|
||||
Containers.pruneImages();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
visible: Containers.unusedVolumes.length > 0
|
||||
label: root.confirmingPrune === "volumes"
|
||||
? "Remove " + Containers.unusedVolumes.length + " volumes?"
|
||||
: "Unused volumes"
|
||||
detail: root.confirmingPrune === "volumes"
|
||||
? "A volume holds data. These are the ones podman reports as referenced by "
|
||||
+ "nothing, but removing them cannot be undone."
|
||||
: Containers.unusedVolumes.length + " volumes, "
|
||||
+ Containers.formatBytes(Number(Containers.disk.volumesReclaimable ?? 0))
|
||||
controlWidth: 230
|
||||
divider: false
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: root.confirmingPrune === "volumes" ? "Keep them" : "Remove…"
|
||||
enabled: !Containers.busy
|
||||
onClicked: root.confirmingPrune =
|
||||
root.confirmingPrune === "volumes" ? "" : "volumes"
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
visible: root.confirmingPrune === "volumes"
|
||||
text: "Remove them"
|
||||
tone: "danger"
|
||||
enabled: !Containers.busy
|
||||
onClicked: {
|
||||
root.confirmingPrune = "";
|
||||
Containers.pruneVolumes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── the stacks ──────────────────────────────────────────────────────
|
||||
SettingsCard {
|
||||
visible: Containers.reachable.length > 0
|
||||
title: Containers.reachable.length === 1
|
||||
? "A container is published to your whole network"
|
||||
: "Containers are published to your whole network"
|
||||
subtitle: "These bind every interface, so any machine on your network can connect. "
|
||||
+ "For a development database, loopback is almost always what you want. "
|
||||
+ "Binding adds an address to the compose file and changes nothing else."
|
||||
|
||||
Repeater {
|
||||
model: Containers.projects
|
||||
|
||||
delegate: SettingsCard {
|
||||
id: projectCard
|
||||
model: Containers.reachable
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
label: String(modelData.container ?? "")
|
||||
detail: "Port " + modelData.hostPort + "/" + String(modelData.protocol ?? "tcp")
|
||||
+ " · " + String(modelData.image ?? "")
|
||||
+ (modelData.configFile
|
||||
? "\n" + Containers.shorten(String(modelData.configFile))
|
||||
: "\nNo compose file is recorded for this container, so it cannot be bound from here.")
|
||||
controlWidth: 250
|
||||
|
||||
readonly property var runningSet:
|
||||
(projectCard.modelData.containers ?? []).filter(c => c.state === "running")
|
||||
readonly property var stoppedSet:
|
||||
(projectCard.modelData.containers ?? []).filter(c => c.state !== "running")
|
||||
readonly property string projectName: String(projectCard.modelData.name ?? "")
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
title: String(projectCard.modelData.title ?? "")
|
||||
subtitle: {
|
||||
const total = Number(projectCard.modelData.total ?? 0);
|
||||
const up = Number(projectCard.modelData.running ?? 0);
|
||||
const where = String(projectCard.modelData.configFile ?? "");
|
||||
const counts = up === 0
|
||||
? total + (total === 1 ? " container, stopped" : " containers, all stopped")
|
||||
: up + " of " + total + " running";
|
||||
return where === "" ? counts : counts + " · " + Containers.shorten(where);
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
label: "The whole stack"
|
||||
detail: projectCard.runningSet.length === 0
|
||||
? "Starts every container this project defines."
|
||||
: "Stopping leaves the containers in place; nothing is removed."
|
||||
controlWidth: 250
|
||||
divider: projectCard.runningSet.length > 0
|
||||
|| projectCard.stoppedSet.length > 0
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Start all"
|
||||
visible: projectCard.stoppedSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.startProject(projectCard.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Restart all"
|
||||
visible: projectCard.runningSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.restartProject(projectCard.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Stop all"
|
||||
visible: projectCard.runningSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.stopProject(projectCard.projectName)
|
||||
}
|
||||
SettingsButton {
|
||||
text: "Open compose file"
|
||||
visible: String(modelData.configFile ?? "") !== ""
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.openFile(String(modelData.configFile))
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: projectCard.runningSet
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.image ?? "") + " · " + root.portSummary(modelData)
|
||||
value: {
|
||||
const health = String(modelData.health ?? "");
|
||||
const up = root.uptimeOf(modelData);
|
||||
if (health !== "" && up !== "")
|
||||
return health + " · " + up;
|
||||
return health !== "" ? health : up;
|
||||
}
|
||||
controlWidth: 250
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Logs"
|
||||
onClicked: Containers.openLogs(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Restart"
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.restart(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Stop"
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.stop(String(modelData.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
visible: projectCard.stoppedSet.length > 0
|
||||
activatable: true
|
||||
divider: root.isExpanded(projectCard.projectName)
|
||||
label: (root.isExpanded(projectCard.projectName) ? "▾ " : "▸ ")
|
||||
+ projectCard.stoppedSet.length
|
||||
+ (projectCard.stoppedSet.length === 1
|
||||
? " stopped container" : " stopped containers")
|
||||
detail: {
|
||||
const bad = projectCard.stoppedSet.filter(c => Number(c.exitCode ?? 0) !== 0);
|
||||
return bad.length === 0
|
||||
? ""
|
||||
: bad.length + (bad.length === 1 ? " exited" : " exited") + " badly.";
|
||||
}
|
||||
onActivated: root.toggleExpanded(projectCard.projectName)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.isExpanded(projectCard.projectName) ? projectCard.stoppedSet : []
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
|
||||
controlWidth: 180
|
||||
divider: index < projectCard.stoppedSet.length - 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Logs"
|
||||
onClicked: Containers.openLogs(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Start"
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.start(String(modelData.name))
|
||||
}
|
||||
}
|
||||
SettingsButton {
|
||||
text: "Bind to localhost"
|
||||
tone: "accent"
|
||||
// Needs both halves of the label to find the entry in
|
||||
// the compose file; the Supabase CLI records neither.
|
||||
visible: String(modelData.configFile ?? "") !== ""
|
||||
&& String(modelData.service ?? "") !== ""
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.bindLocal(
|
||||
String(modelData.project), String(modelData.service))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── containers no compose project claims ────────────────────────────
|
||||
TextRow {
|
||||
visible: Containers.reachable.length > 0
|
||||
label: "After binding"
|
||||
detail: "The change takes effect the next time the stack comes up, because a "
|
||||
+ "published port is fixed when the container is created."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
visible: Containers.loose.length > 0
|
||||
title: "Not part of a project"
|
||||
subtitle: "Started directly rather than by a compose file."
|
||||
// ── finding: disk nothing references ────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
visible: Containers.unusedImages.length > 0 || Containers.unusedVolumes.length > 0
|
||||
title: Containers.formatBytes(Containers.reclaimable) + " nothing is using"
|
||||
subtitle: "Images and volumes no container references. Removing them frees the space; "
|
||||
+ "anything still needed downloads again on next use."
|
||||
|
||||
Repeater {
|
||||
model: Containers.unusedImages.slice(0, 5)
|
||||
|
||||
delegate: TextRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: ""
|
||||
value: Containers.formatBytes(Number(modelData.size ?? 0))
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.unusedImages.length > 5
|
||||
label: "and " + (Containers.unusedImages.length - 5) + " more"
|
||||
detail: ""
|
||||
value: ""
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
label: root.confirmingPrune === "images"
|
||||
? "Remove " + Containers.unusedImages.length + " images?"
|
||||
: "Unused images"
|
||||
detail: root.confirmingPrune === "images"
|
||||
? "Each is removed by name. Nothing that a container references is touched."
|
||||
: Containers.unusedImages.length + " images, "
|
||||
+ Containers.formatBytes(Number(Containers.disk.imagesReclaimable ?? 0))
|
||||
visible: Containers.unusedImages.length > 0
|
||||
controlWidth: 230
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: root.confirmingPrune === "images" ? "Keep them" : "Remove…"
|
||||
enabled: !Containers.busy
|
||||
onClicked: root.confirmingPrune =
|
||||
root.confirmingPrune === "images" ? "" : "images"
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
visible: root.confirmingPrune === "images"
|
||||
text: "Remove them"
|
||||
tone: "danger"
|
||||
enabled: !Containers.busy
|
||||
onClicked: {
|
||||
root.confirmingPrune = "";
|
||||
Containers.pruneImages();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
visible: Containers.unusedVolumes.length > 0
|
||||
label: root.confirmingPrune === "volumes"
|
||||
? "Remove " + Containers.unusedVolumes.length + " volumes?"
|
||||
: "Unused volumes"
|
||||
detail: root.confirmingPrune === "volumes"
|
||||
? "A volume holds data. These are the ones podman reports as referenced by "
|
||||
+ "nothing, but removing them cannot be undone."
|
||||
: Containers.unusedVolumes.length + " volumes, "
|
||||
+ Containers.formatBytes(Number(Containers.disk.volumesReclaimable ?? 0))
|
||||
controlWidth: 230
|
||||
divider: false
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: root.confirmingPrune === "volumes" ? "Keep them" : "Remove…"
|
||||
enabled: !Containers.busy
|
||||
onClicked: root.confirmingPrune =
|
||||
root.confirmingPrune === "volumes" ? "" : "volumes"
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
visible: root.confirmingPrune === "volumes"
|
||||
text: "Remove them"
|
||||
tone: "danger"
|
||||
enabled: !Containers.busy
|
||||
onClicked: {
|
||||
root.confirmingPrune = "";
|
||||
Containers.pruneVolumes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── the stacks ──────────────────────────────────────────────────────
|
||||
|
||||
Repeater {
|
||||
model: Containers.projects
|
||||
|
||||
delegate: SettingsCard {
|
||||
id: projectCard
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property var runningSet:
|
||||
(projectCard.modelData.containers ?? []).filter(c => c.state === "running")
|
||||
readonly property var stoppedSet:
|
||||
(projectCard.modelData.containers ?? []).filter(c => c.state !== "running")
|
||||
readonly property string projectName: String(projectCard.modelData.name ?? "")
|
||||
|
||||
title: String(projectCard.modelData.title ?? "")
|
||||
subtitle: {
|
||||
const total = Number(projectCard.modelData.total ?? 0);
|
||||
const up = Number(projectCard.modelData.running ?? 0);
|
||||
const where = String(projectCard.modelData.configFile ?? "");
|
||||
const counts = up === 0
|
||||
? total + (total === 1 ? " container, stopped" : " containers, all stopped")
|
||||
: up + " of " + total + " running";
|
||||
return where === "" ? counts : counts + " · " + Containers.shorten(where);
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
label: "The whole stack"
|
||||
detail: projectCard.runningSet.length === 0
|
||||
? "Starts every container this project defines."
|
||||
: "Stopping leaves the containers in place; nothing is removed."
|
||||
controlWidth: 250
|
||||
divider: projectCard.runningSet.length > 0
|
||||
|| projectCard.stoppedSet.length > 0
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Start all"
|
||||
visible: projectCard.stoppedSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.startProject(projectCard.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Restart all"
|
||||
visible: projectCard.runningSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.restartProject(projectCard.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Stop all"
|
||||
visible: projectCard.runningSet.length > 0
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.stopProject(projectCard.projectName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Containers.loose
|
||||
model: projectCard.runningSet
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
|
||||
controlWidth: 180
|
||||
divider: index < Containers.loose.length - 1
|
||||
detail: String(modelData.image ?? "") + " · " + root.portSummary(modelData)
|
||||
value: {
|
||||
const health = String(modelData.health ?? "");
|
||||
const up = root.uptimeOf(modelData);
|
||||
if (health !== "" && up !== "")
|
||||
return health + " · " + up;
|
||||
return health !== "" ? health : up;
|
||||
}
|
||||
controlWidth: 250
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
@@ -435,122 +340,217 @@ Item {
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: modelData.state === "running" ? "Stop" : "Start"
|
||||
text: "Restart"
|
||||
enabled: !Containers.busy
|
||||
onClicked: modelData.state === "running"
|
||||
? Containers.stop(String(modelData.name))
|
||||
: Containers.start(String(modelData.name))
|
||||
onClicked: Containers.restart(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Stop"
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.stop(String(modelData.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
visible: projectCard.stoppedSet.length > 0
|
||||
activatable: true
|
||||
divider: root.isExpanded(projectCard.projectName)
|
||||
label: (root.isExpanded(projectCard.projectName) ? "▾ " : "▸ ")
|
||||
+ projectCard.stoppedSet.length
|
||||
+ (projectCard.stoppedSet.length === 1
|
||||
? " stopped container" : " stopped containers")
|
||||
detail: {
|
||||
const bad = projectCard.stoppedSet.filter(c => Number(c.exitCode ?? 0) !== 0);
|
||||
return bad.length === 0
|
||||
? ""
|
||||
: bad.length + (bad.length === 1 ? " exited" : " exited") + " badly.";
|
||||
}
|
||||
onActivated: root.toggleExpanded(projectCard.projectName)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.isExpanded(projectCard.projectName) ? projectCard.stoppedSet : []
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
|
||||
controlWidth: 180
|
||||
divider: index < projectCard.stoppedSet.length - 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Logs"
|
||||
onClicked: Containers.openLogs(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "Start"
|
||||
enabled: !Containers.busy
|
||||
onClicked: Containers.start(String(modelData.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.scanned && Containers.available && Containers.total === 0
|
||||
label: "No containers"
|
||||
detail: "Nothing has been created on this machine yet."
|
||||
value: ""
|
||||
divider: false
|
||||
// ── containers no compose project claims ────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
visible: Containers.loose.length > 0
|
||||
title: "Not part of a project"
|
||||
subtitle: "Started directly rather than by a compose file."
|
||||
|
||||
Repeater {
|
||||
model: Containers.loose
|
||||
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
|
||||
controlWidth: 180
|
||||
divider: index < Containers.loose.length - 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Logs"
|
||||
onClicked: Containers.openLogs(String(modelData.name))
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: modelData.state === "running" ? "Stop" : "Start"
|
||||
enabled: !Containers.busy
|
||||
onClicked: modelData.state === "running"
|
||||
? Containers.stop(String(modelData.name))
|
||||
: Containers.start(String(modelData.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Containers.scanned && Containers.available && Containers.total === 0
|
||||
label: "No containers"
|
||||
detail: "Nothing has been created on this machine yet."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
// ── logs ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A drill-in rather than a panel inside the scrolling page: logs need their
|
||||
// own scrollback, and nesting one scrolling view inside another makes the
|
||||
// wheel ambiguous over the region where you most want to use it.
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: Containers.logTarget !== ""
|
||||
drillIn: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
id: logHeader
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: 34
|
||||
anchors.rightMargin: 34
|
||||
anchors.topMargin: 30
|
||||
spacing: 6
|
||||
Column {
|
||||
id: logHeader
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: 34
|
||||
anchors.rightMargin: 34
|
||||
anchors.topMargin: 30
|
||||
spacing: 6
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
SettingsButton {
|
||||
text: "‹ Back"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: Containers.closeLogs()
|
||||
SettingsButton {
|
||||
text: "‹ Back"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: Containers.closeLogs()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Containers.logTarget
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 22
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Containers.logTarget
|
||||
color: Theme.fg
|
||||
width: parent.width
|
||||
text: Containers.logError !== ""
|
||||
? Containers.logError
|
||||
: (Containers.logFollowing
|
||||
? "Following. The last " + Containers.logLines.count + " lines are shown."
|
||||
: "The stream has ended.")
|
||||
color: Containers.logError !== "" ? Theme.danger : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 22
|
||||
font.weight: Font.DemiBold
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: Containers.logError !== ""
|
||||
? Containers.logError
|
||||
: (Containers.logFollowing
|
||||
? "Following. The last " + Containers.logLines.count + " lines are shown."
|
||||
: "The stream has ended.")
|
||||
color: Containers.logError !== "" ? Theme.danger : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: logHeader.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 34
|
||||
anchors.rightMargin: 34
|
||||
anchors.topMargin: 14
|
||||
anchors.bottomMargin: 30
|
||||
radius: Theme.cardRadius + 2
|
||||
color: Theme.alpha(Theme.bgDark, 0.75)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: logHeader.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 34
|
||||
anchors.rightMargin: 34
|
||||
anchors.topMargin: 14
|
||||
anchors.bottomMargin: 30
|
||||
radius: Theme.cardRadius + 2
|
||||
color: Theme.alpha(Theme.bgDark, 0.75)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||
ListView {
|
||||
id: logList
|
||||
|
||||
ListView {
|
||||
id: logList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
clip: true
|
||||
model: Containers.logLines
|
||||
spacing: 1
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 400
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
clip: true
|
||||
model: Containers.logLines
|
||||
spacing: 1
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 400
|
||||
// Stay pinned to the newest line while the reader is already at
|
||||
// the bottom, and leave the view alone the moment they scroll up
|
||||
// to read something.
|
||||
property bool pinned: true
|
||||
onContentYChanged: logList.pinned =
|
||||
logList.contentY >= logList.contentHeight - logList.height - 24
|
||||
onCountChanged: if (logList.pinned) logList.positionViewAtEnd()
|
||||
|
||||
// Stay pinned to the newest line while the reader is already at
|
||||
// the bottom, and leave the view alone the moment they scroll up
|
||||
// to read something.
|
||||
property bool pinned: true
|
||||
onContentYChanged: logList.pinned =
|
||||
logList.contentY >= logList.contentHeight - logList.height - 24
|
||||
onCountChanged: if (logList.pinned) logList.positionViewAtEnd()
|
||||
|
||||
delegate: Text {
|
||||
required property string line
|
||||
width: logList.width - 24
|
||||
text: line
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WrapAnywhere
|
||||
textFormat: Text.PlainText
|
||||
delegate: Text {
|
||||
required property string line
|
||||
width: logList.width - 24
|
||||
text: line
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WrapAnywhere
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user