From ba868b6279613c08e69c05ccbb4ab0ed85873c95 Mon Sep 17 00:00:00 2001 From: Justin Wiebe Date: Thu, 2 Jan 2025 20:30:58 -0600 Subject: [PATCH] Add resource usage stats to the compose page * Also fix bug where container status wasn't showing when `docker compose ps` returned an array --- .../docker-socket-handler.ts | 15 +++ backend/dockge-server.ts | 29 ++++++ backend/stack.ts | 19 +++- frontend/components.d.ts | 1 + frontend/src/components/Container.vue | 60 +++++++++++- frontend/src/components/DockerStat.vue | 94 +++++++++++++++++++ frontend/src/icon.ts | 2 + frontend/src/lang/en.json | 7 +- frontend/src/pages/Compose.vue | 27 +++++- 9 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/DockerStat.vue diff --git a/backend/agent-socket-handlers/docker-socket-handler.ts b/backend/agent-socket-handlers/docker-socket-handler.ts index 81746019..a8eb75a0 100644 --- a/backend/agent-socket-handlers/docker-socket-handler.ts +++ b/backend/agent-socket-handlers/docker-socket-handler.ts @@ -238,6 +238,21 @@ export class DockerSocketHandler extends AgentSocketHandler { } }); + // Docker stats + agentSocket.on("dockerStats", async (callback) => { + try { + checkLogin(socket); + + const dockerStats = Object.fromEntries(await server.getDockerStats()); + callbackResult({ + ok: true, + dockerStats, + }, callback); + } catch (e) { + callbackError(e, callback); + } + }); + // getExternalNetworkList agentSocket.on("getDockerNetworkList", async (callback) => { try { diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 8f734ccf..ecc6aa7d 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -631,6 +631,35 @@ export class DockgeServer { return list; } + async getDockerStats() : Promise> { + let stats = new Map(); + + try { + let res = await childProcessAsync.spawn("docker", [ "stats", "--format", "json", "--no-stream" ], { + encoding: "utf-8", + }); + + if (!res.stdout) { + return stats; + } + + let lines = res.stdout?.toString().split("\n"); + + for (let line of lines) { + try { + let obj = JSON.parse(line); + stats.set(obj.Name, obj); + } catch (e) { + } + } + + return stats; + } catch (e) { + log.error("getDockerStats", e); + return stats; + } + } + get stackDirFullPath() { return path.resolve(this.stacksDir); } diff --git a/backend/stack.ts b/backend/stack.ts index fbce5002..f5883c9a 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -497,7 +497,7 @@ export class Stack { } async getServiceStatusList() { - let statusList = new Map(); + let statusList = new Map>(); try { let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], { @@ -511,13 +511,23 @@ export class Stack { let lines = res.stdout?.toString().split("\n"); + const addLine = (obj: { Service: string, State: string, Name: string, Health: string }) => { + if (!statusList.has(obj.Service)) { + statusList.set(obj.Service, []); + } + statusList.get(obj.Service)?.push({ + status: obj.Health || obj.State, + name: obj.Name + }); + }; + for (let line of lines) { try { let obj = JSON.parse(line); - if (obj.Health === "") { - statusList.set(obj.Service, obj.State); + if (obj instanceof Array) { + obj.forEach(addLine); } else { - statusList.set(obj.Service, obj.Health); + addLine(obj); } } catch (e) { } @@ -528,6 +538,5 @@ export class Stack { log.error("getServiceStatusList", e); return statusList; } - } } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 708dd4e0..2adfd0bc 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -16,6 +16,7 @@ declare module 'vue' { BModal: typeof import('bootstrap-vue-next')['BModal'] Confirm: typeof import('./src/components/Confirm.vue')['default'] Container: typeof import('./src/components/Container.vue')['default'] + DockerStat: typeof import('./src/components/DockerStat.vue')['default'] General: typeof import('./src/components/settings/General.vue')['default'] HiddenInput: typeof import('./src/components/HiddenInput.vue')['default'] Login: typeof import('./src/components/Login.vue')['default'] diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index 0bedae5e..5b653d8e 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -35,6 +35,32 @@ {{ $t("deleteContainer") }} +
+
+ +
+ +
+
+ +
+ +
+
+
@@ -138,10 +164,12 @@ import { defineComponent } from "vue"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { parseDockerPort } from "../../../common/util-common"; +import DockerStat from "./DockerStat.vue"; export default defineComponent({ components: { FontAwesomeIcon, + DockerStat }, props: { name: { @@ -156,9 +184,13 @@ export default defineComponent({ type: Boolean, default: false, }, - status: { - type: String, - default: "N/A", + serviceStatus: { + type: Object, + default: null, + }, + dockerStats: { + type: Object, + default: null } }, emits: [ @@ -166,6 +198,7 @@ export default defineComponent({ data() { return { showConfig: false, + expandedStats: false, }; }, computed: { @@ -266,6 +299,22 @@ export default defineComponent({ return ""; } }, + statsInstances() { + if (!this.serviceStatus) { + return []; + } + + return this.serviceStatus + .map(s => this.dockerStats[s.name]) + .filter(s => !!s) + .sort((a, b) => a.Name.localeCompare(b.Name)); + }, + status() { + if (!this.serviceStatus) { + return "N/A"; + } + return this.serviceStatus[0].status; + } }, mounted() { if (this.first) { @@ -308,5 +357,10 @@ export default defineComponent({ align-items: center; justify-content: end; } + + .stats { + font-size: 0.8rem; + color: #6c757d; + } } diff --git a/frontend/src/components/DockerStat.vue b/frontend/src/components/DockerStat.vue new file mode 100644 index 00000000..36a82b89 --- /dev/null +++ b/frontend/src/components/DockerStat.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/icon.ts b/frontend/src/icon.ts index 0599e6af..6380eac9 100644 --- a/frontend/src/icon.ts +++ b/frontend/src/icon.ts @@ -38,6 +38,7 @@ import { faAward, faLink, faChevronDown, + faChevronUp, faSignOutAlt, faPen, faExternalLinkSquareAlt, @@ -88,6 +89,7 @@ library.add( faAward, faLink, faChevronDown, + faChevronUp, faSignOutAlt, faPen, faExternalLinkSquareAlt, diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 06362264..e6153deb 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -128,5 +128,10 @@ "New Container Name...": "New Container Name...", "Network name...": "Network name...", "Select a network...": "Select a network...", - "NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first." + "NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first.", + "CPU": "CPU", + "memory": "Memory", + "memoryAbbreviated": "Mem", + "networkIO": "Network I/O", + "blockIO": "Block I/O" } diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index 5c632c94..c8b2278d 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -128,7 +128,8 @@ :name="name" :is-edit-mode="isEditMode" :first="name === Object.keys(jsonConfig.services)[0]" - :status="serviceStatusList[name]" + :serviceStatus="serviceStatusList[name]" + :dockerStats="dockerStats" />
@@ -271,6 +272,7 @@ const envDefault = "# VARIABLE=value #comment"; let yamlErrorTimeout = null; let serviceStatusTimeout = null; +let dockerStatsTimeout = null; let prismjsSymbolDefinition = { "symbol": { pattern: /(? { + this.requestDockerStats(); + }, 5000); + }, + requestServiceStatus() { this.$root.emitAgent(this.endpoint, "serviceStatusList", this.stack.name, (res) => { if (res.ok) { @@ -501,6 +513,17 @@ export default { }); }, + requestDockerStats() { + this.$root.emitAgent(this.endpoint, "dockerStats", (res) => { + if (res.ok) { + this.dockerStats = res.dockerStats; + } + if (!this.stopDockerStatsTimeout) { + this.startDockerStatsTimeout(); + } + }); + }, + exitConfirm(next) { if (this.isEditMode) { if (confirm("You are currently editing a stack. Are you sure you want to leave?")) { @@ -518,7 +541,9 @@ export default { exitAction() { console.log("exitAction"); this.stopServiceStatusTimeout = true; + this.stopDockerStatsTimeout = true; clearTimeout(serviceStatusTimeout); + clearTimeout(dockerStatsTimeout); // Leave Combined Terminal console.debug("leaveCombinedTerminal", this.endpoint, this.stack.name);