mirror of
https://github.com/louislam/dockge.git
synced 2026-05-21 14:02:17 +00:00
Add resource usage stats to the compose page (#700)
Co-authored-by: cmcooper1980 <31871143+cmcooper1980@users.noreply.github.com>
This commit is contained in:
@@ -240,6 +240,22 @@ 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);
|
||||
server.sendStackList();
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// Start a service
|
||||
agentSocket.on("startService", async (stackName: unknown, serviceName: unknown, callback) => {
|
||||
try {
|
||||
|
||||
@@ -637,6 +637,35 @@ export class DockgeServer {
|
||||
return list;
|
||||
}
|
||||
|
||||
async getDockerStats() : Promise<Map<string, object>> {
|
||||
let stats = new Map<string, object>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ export class Stack {
|
||||
}
|
||||
|
||||
async getServiceStatusList() {
|
||||
let statusList = new Map<string, { state: string, ports: string[] }>();
|
||||
let statusList = new Map<string, Array<object>>();
|
||||
|
||||
try {
|
||||
let res = await childProcessAsync.spawn("docker", this.getComposeOptions("ps", "--format", "json"), {
|
||||
@@ -522,22 +522,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);
|
||||
let ports = (obj.Ports as string).split(/,\s*/).filter((s) => {
|
||||
return s.indexOf("->") >= 0;
|
||||
});
|
||||
if (obj.Health === "") {
|
||||
statusList.set(obj.Service, {
|
||||
state: obj.State,
|
||||
ports: ports
|
||||
});
|
||||
if (obj instanceof Array) {
|
||||
obj.forEach(addLine);
|
||||
} else {
|
||||
statusList.set(obj.Service, {
|
||||
state: obj.Health,
|
||||
ports: ports
|
||||
});
|
||||
addLine(obj);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
@@ -548,7 +549,6 @@ export class Stack {
|
||||
log.error("getServiceStatusList", e);
|
||||
return statusList;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async startService(socket: DockgeSocket, serviceName: string) {
|
||||
|
||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -19,6 +19,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']
|
||||
GlobalEnv: typeof import('./src/components/settings/GlobalEnv.vue')['default']
|
||||
HiddenInput: typeof import('./src/components/HiddenInput.vue')['default']
|
||||
|
||||
@@ -58,6 +58,32 @@
|
||||
{{ $t("deleteContainer") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="statsInstances.length > 0" class="mt-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<template v-if="!expandedStats">
|
||||
<div class="stats">
|
||||
{{ $t('CPU') }}: {{ statsInstances[0].CPUPerc }}
|
||||
</div>
|
||||
<div class="stats">
|
||||
{{ $t('memoryAbbreviated') }}: {{ statsInstances[0].MemUsage }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="d-flex flex-grow-1 justify-content-end">
|
||||
<button class="btn btn-sm btn-normal" @click="expandedStats = !expandedStats">
|
||||
<font-awesome-icon :icon="expandedStats ? 'chevron-up' : 'chevron-down'" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="expandedStats" class="d-flex flex-column gap-3 mt-2">
|
||||
<DockerStat
|
||||
v-for="stat in statsInstances"
|
||||
:key="stat.Name"
|
||||
:stat="stat"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="isEditMode && showConfig" class="config mt-3">
|
||||
@@ -161,10 +187,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: {
|
||||
@@ -179,16 +207,12 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: "N/A",
|
||||
serviceStatus: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
processing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ports: {
|
||||
type: Array,
|
||||
dockerStats: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
@@ -200,6 +224,7 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
showConfig: false,
|
||||
expandedStats: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -304,6 +329,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) {
|
||||
@@ -356,5 +397,10 @@ export default defineComponent({
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
94
frontend/src/components/DockerStat.vue
Normal file
94
frontend/src/components/DockerStat.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="stats-container">
|
||||
<div class="stats-title">
|
||||
{{ stat.Name }}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between stats gap-2 mt-1">
|
||||
<div class="stat">
|
||||
<div class="stat-label">
|
||||
{{ $t('CPU') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ stat.CPUPerc }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">
|
||||
{{ $t('memory') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ stat.MemUsage }} ({{ stat.MemPerc }})
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">
|
||||
{{ $t('networkIO') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ stat.NetIO }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">
|
||||
{{ $t('blockIO') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ stat.BlockIO }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
stat: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats-container {
|
||||
container-type: inline-size;
|
||||
|
||||
.stats {
|
||||
container-type: inline-size;
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@container (width < 420px) {
|
||||
flex-direction: column;
|
||||
|
||||
.stat {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.stat-label::after {
|
||||
content: ':'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 0.9rem;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
</style>
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSignOutAlt,
|
||||
faPen,
|
||||
faExternalLinkSquareAlt,
|
||||
@@ -90,6 +91,7 @@ library.add(
|
||||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSignOutAlt,
|
||||
faPen,
|
||||
faExternalLinkSquareAlt,
|
||||
|
||||
@@ -133,6 +133,11 @@
|
||||
"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.",
|
||||
"CPU": "CPU",
|
||||
"memory": "Memory",
|
||||
"memoryAbbreviated": "Mem",
|
||||
"networkIO": "Network I/O",
|
||||
"blockIO": "Block I/O",
|
||||
"Console is not enabled": "Console is not enabled",
|
||||
"ConsoleNotEnabledMSG1": "Console is a powerful tool that allows you to execute any commands such as <code>docker</code>, <code>rm</code> within the Dockge's container in this Web UI.",
|
||||
"ConsoleNotEnabledMSG2": "It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like <code>rm -rf</code>" ,
|
||||
|
||||
@@ -128,12 +128,11 @@
|
||||
:name="name"
|
||||
:is-edit-mode="isEditMode"
|
||||
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||
:processing="processing"
|
||||
:serviceStatus="serviceStatusList[name]"
|
||||
:dockerStats="dockerStats"
|
||||
@start-service="startService"
|
||||
@stop-service="stopService"
|
||||
@restart-service="restartService"
|
||||
:status="serviceStatusList[name]?.state"
|
||||
:ports="serviceStatusList[name]?.ports"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -282,6 +281,12 @@ const envDefault = "# VARIABLE=value #comment";
|
||||
let yamlErrorTimeout = null;
|
||||
|
||||
let serviceStatusTimeout = null;
|
||||
let dockerStatsTimeout = null;
|
||||
let prismjsSymbolDefinition = {
|
||||
"symbol": {
|
||||
pattern: /(?<!\$)\$(\{[^{}]*\}|\w+)/,
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -337,11 +342,13 @@ export default {
|
||||
|
||||
},
|
||||
serviceStatusList: {},
|
||||
dockerStats: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
showDeleteDialog: false,
|
||||
newContainerName: "",
|
||||
stopServiceStatusTimeout: false,
|
||||
stopDockerStatsTimeout: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -508,6 +515,7 @@ export default {
|
||||
}
|
||||
|
||||
this.requestServiceStatus();
|
||||
this.requestDockerStats();
|
||||
},
|
||||
unmounted() {
|
||||
|
||||
@@ -520,6 +528,13 @@ export default {
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
startDockerStatsTimeout() {
|
||||
clearTimeout(dockerStatsTimeout);
|
||||
dockerStatsTimeout = setTimeout(async () => {
|
||||
this.requestDockerStats();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
requestServiceStatus() {
|
||||
// Do not request if it is add mode
|
||||
if (this.isAdd) {
|
||||
@@ -536,6 +551,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(this.$t("confirmLeaveStack"))) {
|
||||
@@ -553,7 +579,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);
|
||||
|
||||
Reference in New Issue
Block a user