Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions backend/agent-socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export class DockerSocketHandler extends AgentSocketHandler {
msgi18n: true,
}, callback);
server.sendStackList();

stack.leaveCombinedTerminal(socket);
} catch (e) {
callbackError(e, callback);
}
Expand Down Expand Up @@ -238,6 +240,68 @@ export class DockerSocketHandler extends AgentSocketHandler {
}
});

// Start a service
agentSocket.on("startService", async (stackName: unknown, serviceName: unknown, callback) => {
try {
checkLogin(socket);

if (typeof (stackName) !== "string" || typeof (serviceName) !== "string") {
throw new ValidationError("Stack name and service name must be strings");
}

const stack = await Stack.getStack(server, stackName);
await stack.startService(socket, serviceName);
stack.joinCombinedTerminal(socket); // Ensure the combined terminal is joined
callbackResult({
ok: true,
msg: "Service" + serviceName + " started"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg: "Service" + serviceName + " started"
msg: "Service " + serviceName + " started"

}, callback);
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});

// Stop a service
agentSocket.on("stopService", async (stackName: unknown, serviceName: unknown, callback) => {
try {
checkLogin(socket);

if (typeof (stackName) !== "string" || typeof (serviceName) !== "string") {
throw new ValidationError("Stack name and service name must be strings");
}

const stack = await Stack.getStack(server, stackName);
await stack.stopService(socket, serviceName);
callbackResult({
ok: true,
msg: "Service" + serviceName + " stopped"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg: "Service" + serviceName + " stopped"
msg: "Service " + serviceName + " stopped"

}, callback);
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});

agentSocket.on("restartService", async (stackName: unknown, serviceName: unknown, callback) => {
try {
checkLogin(socket);

if (typeof stackName !== "string" || typeof serviceName !== "string") {
throw new Error("Invalid stackName or serviceName");
}

const stack = await Stack.getStack(server, stackName, true);
await stack.restartService(socket, serviceName);
callbackResult({
ok: true,
msg: "Service" + serviceName + " restarted"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg: "Service" + serviceName + " restarted"
msg: "Service " + serviceName + " restarted"

}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// getExternalNetworkList
agentSocket.on("getDockerNetworkList", async (callback) => {
try {
Expand Down
30 changes: 30 additions & 0 deletions backend/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,34 @@ export class Stack {
}

}

async startService(socket: DockgeSocket, serviceName: string) {
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "up", "-d", serviceName], this.path);
if (exitCode !== 0) {
throw new Error(`Failed to start service ${serviceName}, please check logs for more information.`);
}

return exitCode;
}

async stopService(socket: DockgeSocket, serviceName: string): Promise<number> {
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "stop", serviceName], this.path);
if (exitCode !== 0) {
throw new Error(`Failed to stop service ${serviceName}, please check logs for more information.`);
}

return exitCode;
}

async restartService(socket: DockgeSocket, serviceName: string): Promise<number> {
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
const exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", ["compose", "restart", serviceName], this.path);
if (exitCode !== 0) {
throw new Error(`Failed to restart service ${serviceName}, please check logs for more information.`);
}

return exitCode;
}
}
35 changes: 33 additions & 2 deletions frontend/src/components/Container.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="shadow-box big-padding mb-3 container">
<div class="row">
<div class="col-7">
<div class="col-5">
<h4>{{ name }}</h4>
<div class="image mb-2">
<span class="me-1">{{ imageName }}:</span><span class="tag">{{ imageTag }}</span>
Expand All @@ -14,12 +14,33 @@
</a>
</div>
</div>
<div class="col-5">
<div class="col-7">
<div class="function">
<router-link v-if="!isEditMode" class="btn btn-normal" :to="terminalRouteLink" disabled="">
<font-awesome-icon icon="terminal" />
Bash
</router-link>
<button v-if="status !== 'running' && status !== 'healthy'"
class="btn btn-primary me-2"
:disabled="processing"
@click="startService">
<font-awesome-icon icon="play" class="me-1" />
Start
</button>
<button v-if="status === 'running' || status === 'healthy' || status === 'unhealthy'"
class="btn btn-danger me-2"
:disabled="processing"
@click="stopService">
<font-awesome-icon icon="stop" class="me-1" />
Stop
</button>
<button v-if="status === 'running' || status === 'healthy' || status === 'unhealthy'"
class="btn btn-warning me-2"
:disabled="processing"
@click="restartService">
<font-awesome-icon icon="sync" class="me-1" />
Restart
</button>
Comment on lines 19 to +43
Copy link

@Dracrius Dracrius Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A much cleaner and more consistent look would be to use a button group like the existing set for the entire stack. This can be done like so. This also hides the bash button when a container isn't running if used as written as it does nothing but tell you the container isn't running.

Suggested change
<router-link v-if="!isEditMode" class="btn btn-normal" :to="terminalRouteLink" disabled="">
<font-awesome-icon icon="terminal" />
Bash
</router-link>
<button v-if="status !== 'running' && status !== 'healthy'"
class="btn btn-primary me-2"
:disabled="processing"
@click="startService">
<font-awesome-icon icon="play" class="me-1" />
Start
</button>
<button v-if="status === 'running' || status === 'healthy' || status === 'unhealthy'"
class="btn btn-danger me-2"
:disabled="processing"
@click="stopService">
<font-awesome-icon icon="stop" class="me-1" />
Stop
</button>
<button v-if="status === 'running' || status === 'healthy' || status === 'unhealthy'"
class="btn btn-warning me-2"
:disabled="processing"
@click="restartService">
<font-awesome-icon icon="sync" class="me-1" />
Restart
</button>
<div class="btn-group me-2" role="group">
<router-link v-if="!isEditMode && (status === 'running' || status === 'healthy')" class="btn btn-normal" :to="terminalRouteLink" disabled="">
<font-awesome-icon icon="terminal" />
Bash
</router-link>
<button v-if="!isEditMode && status !== 'running' && status !== 'healthy'"
class="btn btn-primary"
:disabled="processing"
@click="startService">
<font-awesome-icon icon="play" class="me-1" />
{{ $t("startStack") }}
</button>
<button v-if="!isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
class="btn btn-normal"
:disabled="processing"
@click="restartService">
<font-awesome-icon icon="rotate" class="me-1" />
{{ $t("restartStack") }}
</button>
<button v-if="!isEditMode && (status === 'running' || status === 'healthy' || status === 'unhealthy')"
class="btn btn-normal"
:disabled="processing"
@click="stopService">
<font-awesome-icon icon="stop" class="me-1" />
{{ $t("stopStack") }}
</button>
</div>

image

Copy link

@Dracrius Dracrius Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I forgot this also uses the localized names for Play, Restart and Stop. Bash uses a hard coded label because it doesn't get localized being a name not a word.

</div>
</div>
</div>
Expand Down Expand Up @@ -284,6 +305,16 @@ export default defineComponent({
remove() {
delete this.jsonObject.services[this.name];
},
startService() {
this.$emit("start-service", this.name);
},
stopService() {
this.$emit("stop-service", this.name);
},
restartService() {
this.$emit("restart-service", this.name);
}

}
});
</script>
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/pages/Compose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
:is-edit-mode="isEditMode"
:first="name === Object.keys(jsonConfig.services)[0]"
:status="serviceStatusList[name]"
:processing="processing"
@start-service="startService"
@stop-service="stopService"
@restart-service="restartService"
/>
</div>

Expand Down Expand Up @@ -786,6 +790,44 @@ export default {
this.stack.name = this.stack?.name?.toLowerCase();
},

startService(serviceName) {
this.processing = true;

this.$root.emitAgent(this.endpoint, "startService", this.stack.name, serviceName, (res) => {
this.processing = false;
this.$root.toastRes(res);

if (res.ok) {
this.requestServiceStatus(); // Refresh service status
}
});
},

stopService(serviceName) {
this.processing = true;

this.$root.emitAgent(this.endpoint, "stopService", this.stack.name, serviceName, (res) => {
this.processing = false;
this.$root.toastRes(res);

if (res.ok) {
this.requestServiceStatus(); // Refresh service status
}
});
},

restartService(serviceName) {
this.processing = true;

this.$root.emitAgent(this.endpoint, "restartService", this.stack.name, serviceName, (res) => {
this.processing = false;
this.$root.toastRes(res);

if (res.ok) {
this.requestServiceStatus(); // Refresh service status
}
});
},
}
};
</script>
Expand Down