diff --git a/backend/agent-socket-handlers/docker-socket-handler.ts b/backend/agent-socket-handlers/docker-socket-handler.ts index 8174601..f81ee47 100644 --- a/backend/agent-socket-handlers/docker-socket-handler.ts +++ b/backend/agent-socket-handlers/docker-socket-handler.ts @@ -147,6 +147,8 @@ export class DockerSocketHandler extends AgentSocketHandler { msgi18n: true, }, callback); server.sendStackList(); + + stack.leaveCombinedTerminal(socket); } catch (e) { callbackError(e, callback); } @@ -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" + }, 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" + }, 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" + }, callback); + } catch (e) { + callbackError(e, callback); + } + }); + // getExternalNetworkList agentSocket.on("getDockerNetworkList", async (callback) => { try { diff --git a/backend/stack.ts b/backend/stack.ts index fbce500..bddd012 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -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; + } } diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index 0bedae5..fca36af 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -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> @@ -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> </div> </div> </div> @@ -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> diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index bcea292..e546dc5 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -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> @@ -791,6 +795,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>