mirror of
https://github.com/louislam/dockge.git
synced 2025-02-25 21:15:56 +00:00
Add resource usage stats to the compose page
* Also fix bug where container status wasn't showing when `docker compose ps` returned an array
This commit is contained in:
parent
a65a9f5549
commit
ba868b6279
9 changed files with 244 additions and 10 deletions
|
@ -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 {
|
||||
|
|
|
@ -631,6 +631,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);
|
||||
}
|
||||
|
|
|
@ -497,7 +497,7 @@ export class Stack {
|
|||
}
|
||||
|
||||
async getServiceStatusList() {
|
||||
let statusList = new Map<string, number>();
|
||||
let statusList = new Map<string, Array<object>>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
|
@ -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']
|
||||
|
|
|
@ -35,6 +35,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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
</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,
|
||||
|
@ -88,6 +89,7 @@ library.add(
|
|||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSignOutAlt,
|
||||
faPen,
|
||||
faExternalLinkSquareAlt,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -271,6 +272,7 @@ const envDefault = "# VARIABLE=value #comment";
|
|||
let yamlErrorTimeout = null;
|
||||
|
||||
let serviceStatusTimeout = null;
|
||||
let dockerStatsTimeout = null;
|
||||
let prismjsSymbolDefinition = {
|
||||
"symbol": {
|
||||
pattern: /(?<!\$)\$(\{[^{}]*\}|\w+)/,
|
||||
|
@ -306,11 +308,13 @@ export default {
|
|||
|
||||
},
|
||||
serviceStatusList: {},
|
||||
dockerStats: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
showDeleteDialog: false,
|
||||
newContainerName: "",
|
||||
stopServiceStatusTimeout: false,
|
||||
stopDockerStatsTimeout: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -478,6 +482,7 @@ export default {
|
|||
}
|
||||
|
||||
this.requestServiceStatus();
|
||||
this.requestDockerStats();
|
||||
},
|
||||
unmounted() {
|
||||
|
||||
|
@ -490,6 +495,13 @@ export default {
|
|||
}, 5000);
|
||||
},
|
||||
|
||||
startDockerStatsTimeout() {
|
||||
clearTimeout(dockerStatsTimeout);
|
||||
dockerStatsTimeout = setTimeout(async () => {
|
||||
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);
|
||||
|
|
Loading…
Add table
Reference in a new issue