This commit is contained in:
Justin Wiebe 2025-01-02 20:52:18 -06:00 committed by GitHub
commit b1b4e62c66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 244 additions and 10 deletions

View file

@ -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 {

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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']

View file

@ -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>

View 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>

View file

@ -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,

View file

@ -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"
}

View file

@ -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);