Skip to content

Commit 078f762

Browse files
mizadycmcooper1980
andauthored
Container control buttons (louislam#649)
Co-authored-by: cmcooper1980 <31871143+cmcooper1980@users.noreply.github.com> additional QOL commits by @Dracrius
1 parent e589d4e commit 078f762

4 files changed

Lines changed: 186 additions & 6 deletions

File tree

backend/agent-socket-handlers/docker-socket-handler.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ export class DockerSocketHandler extends AgentSocketHandler {
147147
msgi18n: true,
148148
}, callback);
149149
server.sendStackList();
150+
151+
stack.leaveCombinedTerminal(socket);
150152
} catch (e) {
151153
callbackError(e, callback);
152154
}
@@ -238,6 +240,68 @@ export class DockerSocketHandler extends AgentSocketHandler {
238240
}
239241
});
240242

243+
// Start a service
244+
agentSocket.on("startService", async (stackName: unknown, serviceName: unknown, callback) => {
245+
try {
246+
checkLogin(socket);
247+
248+
if (typeof (stackName) !== "string" || typeof (serviceName) !== "string") {
249+
throw new ValidationError("Stack name and service name must be strings");
250+
}
251+
252+
const stack = await Stack.getStack(server, stackName);
253+
await stack.startService(socket, serviceName);
254+
stack.joinCombinedTerminal(socket); // Ensure the combined terminal is joined
255+
callbackResult({
256+
ok: true,
257+
msg: "Service " + serviceName + " started"
258+
}, callback);
259+
server.sendStackList();
260+
} catch (e) {
261+
callbackError(e, callback);
262+
}
263+
});
264+
265+
// Stop a service
266+
agentSocket.on("stopService", async (stackName: unknown, serviceName: unknown, callback) => {
267+
try {
268+
checkLogin(socket);
269+
270+
if (typeof (stackName) !== "string" || typeof (serviceName) !== "string") {
271+
throw new ValidationError("Stack name and service name must be strings");
272+
}
273+
274+
const stack = await Stack.getStack(server, stackName);
275+
await stack.stopService(socket, serviceName);
276+
callbackResult({
277+
ok: true,
278+
msg: "Service " + serviceName + " stopped"
279+
}, callback);
280+
server.sendStackList();
281+
} catch (e) {
282+
callbackError(e, callback);
283+
}
284+
});
285+
286+
agentSocket.on("restartService", async (stackName: unknown, serviceName: unknown, callback) => {
287+
try {
288+
checkLogin(socket);
289+
290+
if (typeof stackName !== "string" || typeof serviceName !== "string") {
291+
throw new Error("Invalid stackName or serviceName");
292+
}
293+
294+
const stack = await Stack.getStack(server, stackName, true);
295+
await stack.restartService(socket, serviceName);
296+
callbackResult({
297+
ok: true,
298+
msg: "Service " + serviceName + " restarted"
299+
}, callback);
300+
} catch (e) {
301+
callbackError(e, callback);
302+
}
303+
});
304+
241305
// getExternalNetworkList
242306
agentSocket.on("getDockerNetworkList", async (callback) => {
243307
try {

backend/stack.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,4 +550,34 @@ export class Stack {
550550
}
551551

552552
}
553+
554+
async startService(socket: DockgeSocket, serviceName: string) {
555+
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
556+
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "up", "-d", serviceName], this.path);
557+
if (exitCode !== 0) {
558+
throw new Error(`Failed to start service ${serviceName}, please check logs for more information.`);
559+
}
560+
561+
return exitCode;
562+
}
563+
564+
async stopService(socket: DockgeSocket, serviceName: string): Promise<number> {
565+
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
566+
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "stop", serviceName], this.path);
567+
if (exitCode !== 0) {
568+
throw new Error(`Failed to stop service ${serviceName}, please check logs for more information.`);
569+
}
570+
571+
return exitCode;
572+
}
573+
574+
async restartService(socket: DockgeSocket, serviceName: string): Promise<number> {
575+
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
576+
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "restart", serviceName], this.path);
577+
if (exitCode !== 0) {
578+
throw new Error(`Failed to restart service ${serviceName}, please check logs for more information.`);
579+
}
580+
581+
return exitCode;
582+
}
553583
}

frontend/src/components/Container.vue

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="shadow-box big-padding mb-3 container">
33
<div class="row">
4-
<div class="col-7">
4+
<div class="col-5">
55
<h4>{{ name }}</h4>
66
<div class="image mb-2">
77
<span class="me-1">{{ imageName }}:</span><span class="tag">{{ imageTag }}</span>
@@ -14,12 +14,35 @@
1414
</a>
1515
</div>
1616
</div>
17-
<div class="col-5">
17+
<div class="col-7">
1818
<div class="function">
19-
<router-link v-if="!isEditMode" class="btn btn-normal" :to="terminalRouteLink" disabled="">
20-
<font-awesome-icon icon="terminal" />
21-
Bash
22-
</router-link>
19+
<div class="btn-group me-2" role="group">
20+
<router-link v-if="!isEditMode && (status === 'running' || status === 'healthy')" class="btn btn-normal" :to="terminalRouteLink" disabled="">
21+
<font-awesome-icon icon="terminal" />
22+
Bash
23+
</router-link>
24+
<button v-if="this.serviceCount > 1 && !isEditMode && status !== 'running' && status !== 'healthy'"
25+
class="btn btn-primary"
26+
:disabled="processing"
27+
@click="startService">
28+
<font-awesome-icon icon="play" class="me-1" />
29+
{{ $t("startStack") }}
30+
</button>
31+
<button v-if="this.serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
32+
class="btn btn-normal"
33+
:disabled="processing"
34+
@click="restartService">
35+
<font-awesome-icon icon="rotate" class="me-1" />
36+
{{ $t("restartStack") }}
37+
</button>
38+
<button v-if="this.serviceCount > 1 && !isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
39+
class="btn btn-normal"
40+
:disabled="processing"
41+
@click="stopService">
42+
<font-awesome-icon icon="stop" class="me-1" />
43+
{{ $t("stopStack") }}
44+
</button>
45+
</div>
2346
</div>
2447
</div>
2548
</div>
@@ -160,12 +183,19 @@ export default defineComponent({
160183
type: String,
161184
default: "N/A",
162185
},
186+
processing: {
187+
type: Boolean,
188+
default: false,
189+
},
163190
ports: {
164191
type: Array,
165192
default: null
166193
}
167194
},
168195
emits: [
196+
"start-service",
197+
"stop-service",
198+
"restart-service"
169199
],
170200
data() {
171201
return {
@@ -234,6 +264,10 @@ export default defineComponent({
234264
return this.jsonObject.services[this.name];
235265
},
236266
267+
serviceCount() {
268+
return Object.keys(this.jsonObject.services).length;
269+
},
270+
237271
jsonObject() {
238272
return this.$parent.$parent.jsonConfig;
239273
},
@@ -288,6 +322,16 @@ export default defineComponent({
288322
remove() {
289323
delete this.jsonObject.services[this.name];
290324
},
325+
startService() {
326+
this.$emit("start-service", this.name);
327+
},
328+
stopService() {
329+
this.$emit("stop-service", this.name);
330+
},
331+
restartService() {
332+
this.$emit("restart-service", this.name);
333+
}
334+
291335
}
292336
});
293337
</script>

frontend/src/pages/Compose.vue

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@
128128
:name="name"
129129
:is-edit-mode="isEditMode"
130130
:first="name === Object.keys(jsonConfig.services)[0]"
131+
:processing="processing"
132+
@start-service="startService"
133+
@stop-service="stopService"
134+
@restart-service="restartService"
131135
:status="serviceStatusList[name]?.state"
132136
:ports="serviceStatusList[name]?.ports"
133137
/>
@@ -777,6 +781,44 @@ export default {
777781
this.stack.name = this.stack?.name?.toLowerCase();
778782
},
779783
784+
startService(serviceName) {
785+
this.processing = true;
786+
787+
this.$root.emitAgent(this.endpoint, "startService", this.stack.name, serviceName, (res) => {
788+
this.processing = false;
789+
this.$root.toastRes(res);
790+
791+
if (res.ok) {
792+
this.requestServiceStatus(); // Refresh service status
793+
}
794+
});
795+
},
796+
797+
stopService(serviceName) {
798+
this.processing = true;
799+
800+
this.$root.emitAgent(this.endpoint, "stopService", this.stack.name, serviceName, (res) => {
801+
this.processing = false;
802+
this.$root.toastRes(res);
803+
804+
if (res.ok) {
805+
this.requestServiceStatus(); // Refresh service status
806+
}
807+
});
808+
},
809+
810+
restartService(serviceName) {
811+
this.processing = true;
812+
813+
this.$root.emitAgent(this.endpoint, "restartService", this.stack.name, serviceName, (res) => {
814+
this.processing = false;
815+
this.$root.toastRes(res);
816+
817+
if (res.ok) {
818+
this.requestServiceStatus(); // Refresh service status
819+
}
820+
});
821+
},
780822
}
781823
};
782824
</script>

0 commit comments

Comments
 (0)