mirror of
https://github.com/louislam/dockge.git
synced 2024-12-03 15:34:03 +00:00
wip
This commit is contained in:
parent
7d1da2ad99
commit
e67d08b7b3
19 changed files with 483 additions and 104 deletions
|
@ -13,6 +13,7 @@ A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.
|
||||||
- Interactive web terminal for containers and any docker commands
|
- Interactive web terminal for containers and any docker commands
|
||||||
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
|
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
|
||||||
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too
|
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too
|
||||||
|
- Build on top of [Compose V2](https://docs.docker.com/compose/migrate/), as known as `compose.yaml` and `docker compose`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -30,4 +31,6 @@ If you love this project, please consider giving this project a ⭐.
|
||||||
- Container stats
|
- Container stats
|
||||||
- Get app icons
|
- Get app icons
|
||||||
- Switch Docker context
|
- Switch Docker context
|
||||||
|
- Support Dockerfile and build
|
||||||
- Zero-config private docker registry
|
- Zero-config private docker registry
|
||||||
|
- Support Docker swarm
|
||||||
|
|
|
@ -26,6 +26,8 @@ import expressStaticGzip from "express-static-gzip";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
|
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
|
||||||
import { Stack } from "./stack";
|
import { Stack } from "./stack";
|
||||||
|
import { Cron } from "croner";
|
||||||
|
import childProcess from "child_process";
|
||||||
|
|
||||||
export class DockgeServer {
|
export class DockgeServer {
|
||||||
app : Express;
|
app : Express;
|
||||||
|
@ -248,6 +250,15 @@ export class DockgeServer {
|
||||||
} else {
|
} else {
|
||||||
log.info("server", `Listening on ${this.config.port}`);
|
log.info("server", `Listening on ${this.config.port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run every 5 seconds
|
||||||
|
const job = Cron("*/2 * * * * *", {
|
||||||
|
protect: true, // Enabled over-run protection.
|
||||||
|
}, () => {
|
||||||
|
log.debug("server", "Cron job running");
|
||||||
|
this.sendStackList(true);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,18 +423,40 @@ export class DockgeServer {
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStackList(socket : DockgeSocket) {
|
sendStackList(useCache = false) {
|
||||||
let room = socket.userID.toString();
|
let stackList = Stack.getStackList(this, useCache);
|
||||||
let stackList = Stack.getStackList(this);
|
let roomList = this.io.sockets.adapter.rooms.keys();
|
||||||
let list = {};
|
for (let room of roomList) {
|
||||||
|
// Check if the room is a number (user id)
|
||||||
for (let stack of stackList) {
|
if (Number(room)) {
|
||||||
list[stack.name] = stack.toSimpleJSON();
|
this.io.to(room).emit("stackList", {
|
||||||
|
ok: true,
|
||||||
|
stackList: Object.fromEntries(stackList),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.io.to(room).emit("stackList", {
|
sendStackStatusList() {
|
||||||
ok: true,
|
let statusList = Stack.getStatusList();
|
||||||
stackList: list,
|
|
||||||
});
|
let roomList = this.io.sockets.adapter.rooms.keys();
|
||||||
|
|
||||||
|
for (let room of roomList) {
|
||||||
|
// Check if the room is a number (user id)
|
||||||
|
if (Number(room)) {
|
||||||
|
log.debug("server", "Send stack status list to room " + room);
|
||||||
|
this.io.to(room).emit("stackStatusList", {
|
||||||
|
ok: true,
|
||||||
|
stackStatusList: Object.fromEntries(statusList),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug("server", "Skip sending stack status list to room " + room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get stackDirFullPath() {
|
||||||
|
return path.resolve(this.stacksDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,6 @@
|
||||||
import { SocketHandler } from "../socket-handler.js";
|
import { SocketHandler } from "../socket-handler.js";
|
||||||
import { DockgeServer } from "../dockge-server";
|
import { DockgeServer } from "../dockge-server";
|
||||||
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||||
import { log } from "../log";
|
|
||||||
import yaml from "yaml";
|
|
||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
import {
|
|
||||||
allowedCommandList,
|
|
||||||
allowedRawKeys,
|
|
||||||
getComposeTerminalName,
|
|
||||||
isDev,
|
|
||||||
PROGRESS_TERMINAL_ROWS
|
|
||||||
} from "../util-common";
|
|
||||||
import { Terminal } from "../terminal";
|
|
||||||
import { Stack } from "../stack";
|
import { Stack } from "../stack";
|
||||||
|
|
||||||
export class DockerSocketHandler extends SocketHandler {
|
export class DockerSocketHandler extends SocketHandler {
|
||||||
|
@ -23,6 +11,7 @@ export class DockerSocketHandler extends SocketHandler {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const stack = this.saveStack(socket, server, name, composeYAML, isAdd);
|
const stack = this.saveStack(socket, server, name, composeYAML, isAdd);
|
||||||
await stack.deploy(socket);
|
await stack.deploy(socket);
|
||||||
|
server.sendStackList();
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
@ -39,7 +28,33 @@ export class DockerSocketHandler extends SocketHandler {
|
||||||
ok: true,
|
ok: true,
|
||||||
"msg": "Saved"
|
"msg": "Saved"
|
||||||
});
|
});
|
||||||
server.sendStackList(socket);
|
server.sendStackList();
|
||||||
|
} catch (e) {
|
||||||
|
callbackError(e, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteStack", async (name : unknown, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (typeof(name) !== "string") {
|
||||||
|
throw new ValidationError("Name must be a string");
|
||||||
|
}
|
||||||
|
const stack = Stack.getStack(server, name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stack.delete(socket);
|
||||||
|
} catch (e) {
|
||||||
|
server.sendStackList();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.sendStackList();
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted"
|
||||||
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callbackError(e, callback);
|
callbackError(e, callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,14 @@ import { log } from "../log";
|
||||||
import yaml from "yaml";
|
import yaml from "yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { allowedCommandList, allowedRawKeys, isDev } from "../util-common";
|
import {
|
||||||
import { Terminal } from "../terminal";
|
allowedCommandList,
|
||||||
|
allowedRawKeys,
|
||||||
|
getComposeTerminalName,
|
||||||
|
isDev,
|
||||||
|
PROGRESS_TERMINAL_ROWS
|
||||||
|
} from "../util-common";
|
||||||
|
import { MainTerminal, Terminal } from "../terminal";
|
||||||
|
|
||||||
export class TerminalSocketHandler extends SocketHandler {
|
export class TerminalSocketHandler extends SocketHandler {
|
||||||
create(socket : DockgeSocket, server : DockgeServer) {
|
create(socket : DockgeSocket, server : DockgeServer) {
|
||||||
|
@ -57,12 +63,36 @@ export class TerminalSocketHandler extends SocketHandler {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Terminal
|
// Create Terminal
|
||||||
socket.on("terminalCreate", async (terminalName : unknown, callback : unknown) => {
|
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (typeof(terminalName) !== "string") {
|
||||||
|
throw new ValidationError("Terminal name must be a string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("deployStack", "Terminal name: " + terminalName);
|
||||||
|
|
||||||
|
let terminal = Terminal.getTerminal(terminalName);
|
||||||
|
|
||||||
|
if (!terminal) {
|
||||||
|
terminal = new MainTerminal(server, terminalName);
|
||||||
|
terminal.rows = 50;
|
||||||
|
log.debug("deployStack", "Terminal created");
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.join(socket);
|
||||||
|
terminal.start();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callbackError(e, callback);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Join Terminal
|
// Join Terminal
|
||||||
socket.on("terminalJoin", async (terminalName : unknown, callback : unknown) => {
|
socket.on("terminalJoin", async (terminalName : unknown, callback) => {
|
||||||
if (typeof(callback) !== "function") {
|
if (typeof(callback) !== "function") {
|
||||||
log.debug("console", "Callback is not a function.");
|
log.debug("console", "Callback is not a function.");
|
||||||
return;
|
return;
|
||||||
|
|
169
backend/stack.ts
169
backend/stack.ts
|
@ -4,15 +4,28 @@ import { log } from "./log";
|
||||||
import yaml from "yaml";
|
import yaml from "yaml";
|
||||||
import { DockgeSocket, ValidationError } from "./util-server";
|
import { DockgeSocket, ValidationError } from "./util-server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "./util-common";
|
import {
|
||||||
|
CREATED_FILE,
|
||||||
|
CREATED_STACK,
|
||||||
|
EXITED,
|
||||||
|
getComposeTerminalName,
|
||||||
|
PROGRESS_TERMINAL_ROWS,
|
||||||
|
RUNNING,
|
||||||
|
UNKNOWN
|
||||||
|
} from "./util-common";
|
||||||
import { Terminal } from "./terminal";
|
import { Terminal } from "./terminal";
|
||||||
|
import childProcess from "child_process";
|
||||||
|
|
||||||
export class Stack {
|
export class Stack {
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
|
protected _status: number = UNKNOWN;
|
||||||
protected _composeYAML?: string;
|
protected _composeYAML?: string;
|
||||||
|
protected _configFilePath?: string;
|
||||||
protected server: DockgeServer;
|
protected server: DockgeServer;
|
||||||
|
|
||||||
|
protected static managedStackList: Map<string, Stack> = new Map();
|
||||||
|
|
||||||
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
@ -24,16 +37,30 @@ export class Stack {
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
composeYAML: this.composeYAML,
|
composeYAML: this.composeYAML,
|
||||||
|
isManagedByDockge: this.isManagedByDockge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toSimpleJSON() : object {
|
toSimpleJSON() : object {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
status: this._status,
|
||||||
tags: [],
|
tags: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isManagedByDockge() : boolean {
|
||||||
|
if (this._configFilePath) {
|
||||||
|
return this._configFilePath.startsWith(this.server.stackDirFullPath) && fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() : number {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
// Check name, allows [a-z][A-Z][0-9] _ - only
|
// Check name, allows [a-z][A-Z][0-9] _ - only
|
||||||
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
|
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||||
|
@ -101,7 +128,7 @@ export class Stack {
|
||||||
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
deploy(socket? : DockgeSocket) : Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(this.name);
|
const terminalName = getComposeTerminalName(this.name);
|
||||||
log.debug("deployStack", "Terminal name: " + terminalName);
|
log.debug("deployStack", "Terminal name: " + terminalName);
|
||||||
|
|
||||||
|
@ -129,30 +156,138 @@ export class Stack {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getStackList(server : DockgeServer) : Stack[] {
|
delete(socket?: DockgeSocket) : Promise<number> {
|
||||||
let stacksDir = server.stacksDir;
|
// Docker compose down
|
||||||
let stackList : Stack[] = [];
|
const terminalName = getComposeTerminalName(this.name);
|
||||||
|
log.debug("deleteStack", "Terminal name: " + terminalName);
|
||||||
|
|
||||||
// Scan the stacks directory, and get the stack list
|
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "down" ], this.path);
|
||||||
let filenameList = fs.readdirSync(stacksDir);
|
|
||||||
|
|
||||||
log.debug("stack", filenameList);
|
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
||||||
|
|
||||||
for (let filename of filenameList) {
|
if (socket) {
|
||||||
let relativePath = path.join(stacksDir, filename);
|
terminal.join(socket);
|
||||||
if (fs.statSync(relativePath).isDirectory()) {
|
log.debug("deployStack", "Terminal joined");
|
||||||
let stack = new Stack(server, filename);
|
} else {
|
||||||
stackList.push(stack);
|
log.debug("deployStack", "No socket, not joining");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
terminal.onExit((exitCode : number) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
// Remove the stack folder
|
||||||
|
try {
|
||||||
|
fs.rmSync(this.path, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
resolve(exitCode);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to delete, please check the terminal output for more information."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
terminal.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
|
||||||
|
let stacksDir = server.stacksDir;
|
||||||
|
let stackList : Map<string, Stack>;
|
||||||
|
|
||||||
|
if (useCacheForManaged && this.managedStackList.size > 0) {
|
||||||
|
stackList = this.managedStackList;
|
||||||
|
} else {
|
||||||
|
stackList = new Map<string, Stack>();
|
||||||
|
|
||||||
|
// Scan the stacks directory, and get the stack list
|
||||||
|
let filenameList = fs.readdirSync(stacksDir);
|
||||||
|
|
||||||
|
for (let filename of filenameList) {
|
||||||
|
try {
|
||||||
|
let stack = this.getStack(server, filename);
|
||||||
|
stack._status = CREATED_FILE;
|
||||||
|
stackList.set(filename, stack);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache by copying
|
||||||
|
this.managedStackList = new Map(stackList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also get the list from `docker compose ls --all --format json`
|
||||||
|
let res = childProcess.execSync("docker compose ls --all --format json");
|
||||||
|
let composeList = JSON.parse(res.toString());
|
||||||
|
|
||||||
|
for (let composeStack of composeList) {
|
||||||
|
let stack = stackList.get(composeStack.Name);
|
||||||
|
|
||||||
|
// This stack probably is not managed by Dockge, but we still want to show it
|
||||||
|
if (!stack) {
|
||||||
|
stack = new Stack(server, composeStack.Name);
|
||||||
|
stackList.set(composeStack.Name, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
stack._status = this.statusConvert(composeStack.Status);
|
||||||
|
stack._configFilePath = composeStack.ConfigFiles;
|
||||||
|
}
|
||||||
|
|
||||||
return stackList;
|
return stackList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status list, it will be used to update the status of the stacks
|
||||||
|
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
||||||
|
*/
|
||||||
|
static getStatusList() : Map<string, number> {
|
||||||
|
let statusList = new Map<string, number>();
|
||||||
|
|
||||||
|
let res = childProcess.execSync("docker compose ls --all --format json");
|
||||||
|
let composeList = JSON.parse(res.toString());
|
||||||
|
|
||||||
|
for (let composeStack of composeList) {
|
||||||
|
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusList;
|
||||||
|
}
|
||||||
|
|
||||||
|
static statusConvert(status : string) : number {
|
||||||
|
if (status.startsWith("created")) {
|
||||||
|
return CREATED_STACK;
|
||||||
|
} else if (status.startsWith("running")) {
|
||||||
|
return RUNNING;
|
||||||
|
} else if (status.startsWith("exited")) {
|
||||||
|
return EXITED;
|
||||||
|
} else {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static getStack(server: DockgeServer, stackName: string) : Stack {
|
static getStack(server: DockgeServer, stackName: string) : Stack {
|
||||||
let dir = path.join(server.stacksDir, stackName);
|
let dir = path.join(server.stacksDir, stackName);
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
throw new ValidationError("Stack not found");
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
||||||
|
// Maybe it is a stack managed by docker compose directly
|
||||||
|
let stackList = this.getStackList(server);
|
||||||
|
let stack = stackList.get(stackName);
|
||||||
|
|
||||||
|
if (stack) {
|
||||||
|
return stack;
|
||||||
|
} else {
|
||||||
|
// Really not found
|
||||||
|
throw new ValidationError("Stack not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Stack(server, stackName);
|
|
||||||
|
let stack = new Stack(server, stackName);
|
||||||
|
stack._status = UNKNOWN;
|
||||||
|
stack._configFilePath = path.resolve(dir);
|
||||||
|
return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,10 @@ export class Terminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
|
if (this._ptyProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._ptyProcess = pty.spawn(this.file, this.args, {
|
this._ptyProcess = pty.spawn(this.file, this.args, {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
|
@ -139,10 +143,12 @@ export class MainTerminal extends InteractiveTerminal {
|
||||||
constructor(server : DockgeServer, name : string, cwd : string = "./") {
|
constructor(server : DockgeServer, name : string, cwd : string = "./") {
|
||||||
let shell;
|
let shell;
|
||||||
|
|
||||||
if (commandExistsSync("pwsh")) {
|
if (os.platform() === "win32") {
|
||||||
shell = "pwsh";
|
if (commandExistsSync("pwsh.exe")) {
|
||||||
} else if (os.platform() === "win32") {
|
shell = "pwsh.exe";
|
||||||
shell = "powershell.exe";
|
} else {
|
||||||
|
shell = "powershell.exe";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
shell = "bash";
|
shell = "bash";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
// For loading dayjs plugins, don't remove event though it is not used in this file
|
/*
|
||||||
|
* Common utilities for backend and frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Init dayjs
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
let randomBytes : (numBytes: number) => Uint8Array;
|
let randomBytes : (numBytes: number) => Uint8Array;
|
||||||
|
|
||||||
|
@ -17,6 +25,58 @@ if (typeof window !== "undefined" && window.crypto) {
|
||||||
randomBytes = (await import("node:crypto")).randomBytes;
|
randomBytes = (await import("node:crypto")).randomBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stack Status
|
||||||
|
export const UNKNOWN = 0;
|
||||||
|
export const CREATED_FILE = 1;
|
||||||
|
export const CREATED_STACK = 2;
|
||||||
|
export const RUNNING = 3;
|
||||||
|
export const EXITED = 4;
|
||||||
|
|
||||||
|
export function statusName(status : number) : string {
|
||||||
|
switch (status) {
|
||||||
|
case CREATED_FILE:
|
||||||
|
return "draft";
|
||||||
|
case CREATED_STACK:
|
||||||
|
return "created_stack";
|
||||||
|
case RUNNING:
|
||||||
|
return "running";
|
||||||
|
case EXITED:
|
||||||
|
return "exited";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusNameShort(status : number) : string {
|
||||||
|
switch (status) {
|
||||||
|
case CREATED_FILE:
|
||||||
|
return "draft";
|
||||||
|
case CREATED_STACK:
|
||||||
|
return "inactive";
|
||||||
|
case RUNNING:
|
||||||
|
return "active";
|
||||||
|
case EXITED:
|
||||||
|
return "inactive";
|
||||||
|
default:
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusColor(status : number) : string {
|
||||||
|
switch (status) {
|
||||||
|
case CREATED_FILE:
|
||||||
|
return "dark";
|
||||||
|
case CREATED_STACK:
|
||||||
|
return "danger";
|
||||||
|
case RUNNING:
|
||||||
|
return "primary";
|
||||||
|
case EXITED:
|
||||||
|
return "danger";
|
||||||
|
default:
|
||||||
|
return "secondary";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isDev = process.env.NODE_ENV === "development";
|
export const isDev = process.env.NODE_ENV === "development";
|
||||||
export const TERMINAL_COLS = 80;
|
export const TERMINAL_COLS = 80;
|
||||||
export const TERMINAL_ROWS = 10;
|
export const TERMINAL_ROWS = 10;
|
||||||
|
@ -30,6 +90,7 @@ export const allowedCommandList : string[] = [
|
||||||
"cd",
|
"cd",
|
||||||
"dir",
|
"dir",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedRawKeys = [
|
export const allowedRawKeys = [
|
||||||
"\u0003", // Ctrl + C
|
"\u0003", // Ctrl + C
|
||||||
];
|
];
|
||||||
|
|
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
|
@ -7,6 +7,8 @@ export {}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
BButton: typeof import('bootstrap-vue-next')['BButton']
|
||||||
|
BModal: typeof import('bootstrap-vue-next')['BModal']
|
||||||
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
||||||
Login: typeof import('./src/components/Login.vue')['default']
|
Login: typeof import('./src/components/Login.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Confirm from "../components/Confirm.vue";
|
import Confirm from "../components/Confirm.vue";
|
||||||
import StackListItem from "../components/StackListItem.vue";
|
import StackListItem from "../components/StackListItem.vue";
|
||||||
|
import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../backend/util-common";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -154,8 +155,30 @@ export default {
|
||||||
return searchTextMatch && activeMatch && tagsMatch;
|
return searchTextMatch && activeMatch && tagsMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter result by active state, weight and alphabetical
|
|
||||||
result.sort((m1, m2) => {
|
result.sort((m1, m2) => {
|
||||||
|
if (m1.status !== m2.status) {
|
||||||
|
if (m2.status === RUNNING) {
|
||||||
|
return 1;
|
||||||
|
} else if (m1.status === RUNNING) {
|
||||||
|
return -1;
|
||||||
|
} else if (m2.status === EXITED) {
|
||||||
|
return 1;
|
||||||
|
} else if (m1.status === EXITED) {
|
||||||
|
return -1;
|
||||||
|
} else if (m2.status === CREATED_STACK) {
|
||||||
|
return 1;
|
||||||
|
} else if (m1.status === CREATED_STACK) {
|
||||||
|
return -1;
|
||||||
|
} else if (m2.status === CREATED_FILE) {
|
||||||
|
return 1;
|
||||||
|
} else if (m1.status === CREATED_FILE) {
|
||||||
|
return -1;
|
||||||
|
} else if (m2.status === UNKNOWN) {
|
||||||
|
return 1;
|
||||||
|
} else if (m1.status === UNKNOWN) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return m1.name.localeCompare(m2.name);
|
return m1.name.localeCompare(m2.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div :class="{ 'dim' : !stack.isManagedByDockge }">
|
||||||
<div :style="depthMargin">
|
<div :style="depthMargin">
|
||||||
<!-- Checkbox -->
|
<!-- Checkbox -->
|
||||||
<div v-if="isSelectMode" class="select-input-wrapper">
|
<div v-if="isSelectMode" class="select-input-wrapper">
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9 col-md-8 small-padding">
|
<div class="col-9 col-md-8 small-padding">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<Uptime :stack="stack" type="24" :pill="true" />
|
<Uptime :stack="stack" :fixed-width="true" />
|
||||||
{{ stackName }}
|
{{ stackName }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="stack.tags.length > 0" class="tags">
|
<div v-if="stack.tags.length > 0" class="tags">
|
||||||
|
@ -36,7 +36,6 @@ import Uptime from "./Uptime.vue";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Uptime
|
Uptime
|
||||||
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
/** Stack this represents */
|
/** Stack this represents */
|
||||||
|
@ -181,4 +180,8 @@ export default {
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default {
|
||||||
rows: this.rows,
|
rows: this.rows,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.terminal.loadAddon(new WebLinksAddon());
|
//this.terminal.loadAddon(new WebLinksAddon());
|
||||||
|
|
||||||
// Bind to a div
|
// Bind to a div
|
||||||
this.terminal.open(this.$refs.terminal);
|
this.terminal.open(this.$refs.terminal);
|
||||||
|
|
|
@ -1,23 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="className" :title="title">{{ uptime }}</span>
|
<span :class="className">{{ statusName }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { statusColor, statusNameShort } from "../../../backend/util-common";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
/** Monitor this represents */
|
stack: {
|
||||||
monitor: {
|
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
/** Type of monitor */
|
fixedWidth: {
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
/** Is this a pill? */
|
|
||||||
pill: {
|
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -25,28 +19,26 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
uptime() {
|
uptime() {
|
||||||
|
return "0.00%";
|
||||||
return this.$t("notAvailableShort");
|
return this.$t("notAvailableShort");
|
||||||
},
|
},
|
||||||
|
|
||||||
color() {
|
color() {
|
||||||
return "secondary";
|
return statusColor(this.stack?.status);
|
||||||
|
},
|
||||||
|
|
||||||
|
statusName() {
|
||||||
|
return this.$t(statusNameShort(this.stack?.status));
|
||||||
},
|
},
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
if (this.pill) {
|
let className = `badge rounded-pill bg-${this.color}`;
|
||||||
return `badge rounded-pill bg-${this.color}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
if (this.fixedWidth) {
|
||||||
|
className += " fixed-width";
|
||||||
|
}
|
||||||
|
return className;
|
||||||
},
|
},
|
||||||
|
|
||||||
title() {
|
|
||||||
if (this.type === "720") {
|
|
||||||
return `30${this.$t("-day")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `24${this.$t("-hour")}`;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -55,4 +47,8 @@ export default {
|
||||||
.badge {
|
.badge {
|
||||||
min-width: 62px;
|
min-width: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-width {
|
||||||
|
width: 62px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,5 +18,7 @@
|
||||||
"editStack": "Edit",
|
"editStack": "Edit",
|
||||||
"discardStack": "Discard",
|
"discardStack": "Discard",
|
||||||
"saveStackDraft": "Save",
|
"saveStackDraft": "Save",
|
||||||
"notAvailableShort" : "N/A"
|
"notAvailableShort" : "N/A",
|
||||||
|
"deleteStackMsg": "Are you sure you want to delete this stack?",
|
||||||
|
"stackNotManagedByDockgeMsg": "This stack is not managed by Dockge."
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import App from "./App.vue";
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
import { FontAwesomeIcon } from "./icon.js";
|
import { FontAwesomeIcon } from "./icon.js";
|
||||||
import { i18n } from "./i18n";
|
import { i18n } from "./i18n";
|
||||||
|
await import("../../backend/util-common");
|
||||||
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
|
@ -25,10 +26,6 @@ import socket from "./mixins/socket";
|
||||||
import lang from "./mixins/lang";
|
import lang from "./mixins/lang";
|
||||||
import theme from "./mixins/theme";
|
import theme from "./mixins/theme";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const app = createApp(rootApp());
|
const app = createApp(rootApp());
|
||||||
|
|
||||||
app.use(Toast, {
|
app.use(Toast, {
|
||||||
|
|
|
@ -203,6 +203,20 @@ export default defineComponent({
|
||||||
this.stackList = res.stackList;
|
this.stackList = res.stackList;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("stackStatusList", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
|
||||||
|
console.log(res.stackStatusList);
|
||||||
|
|
||||||
|
for (let stackName in res.stackStatusList) {
|
||||||
|
const stackObj = this.stackList[stackName];
|
||||||
|
if (stackObj) {
|
||||||
|
stackObj.status = res.stackStatusList[stackName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<transition name="slide-fade" appear>
|
<transition name="slide-fade" appear>
|
||||||
<div>
|
<div>
|
||||||
<h1 v-if="isAdd" class="mb-3">Compose</h1>
|
<h1 v-if="isAdd" class="mb-3">Compose</h1>
|
||||||
<h1 v-else class="mb-3">Stack: {{ stack.name }}</h1>
|
<h1 v-else class="mb-3"><Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}</h1>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div v-if="stack.isManagedByDockge" class="mb-3">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
|
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
|
||||||
<font-awesome-icon icon="rocket" class="me-1" />
|
<font-awesome-icon icon="rocket" class="me-1" />
|
||||||
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
|
||||||
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
||||||
|
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("updateStack") }}</button>
|
||||||
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
|
||||||
<button v-if="!isEditMode" class="btn btn-primary " :disabled="processing">{{ $t("restartStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-primary " :disabled="processing">{{ $t("restartStack") }}</button>
|
||||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("stopStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("stopStack") }}</button>
|
||||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("deleteStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing" @click="showDeleteDialog = !showDeleteDialog">{{ $t("deleteStack") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,18 +34,20 @@
|
||||||
:allow-input="false"
|
:allow-input="false"
|
||||||
class="mb-3 terminal"
|
class="mb-3 terminal"
|
||||||
:rows="progressTerminalRows"
|
:rows="progressTerminalRows"
|
||||||
@has-data="showProgressTerminal = true"
|
@has-data="showProgressTerminal = true; submitted = true;"
|
||||||
></Terminal>
|
></Terminal>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div class="row">
|
<div v-if="stack.isManagedByDockge" class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="mb-3">General</h4>
|
<div v-if="isAdd">
|
||||||
<div class="shadow-box big-padding mb-3">
|
<h4 class="mb-3">General</h4>
|
||||||
<!-- Stack Name -->
|
<div class="shadow-box big-padding mb-3">
|
||||||
<div v-if="isAdd" class="mb-3">
|
<!-- Stack Name -->
|
||||||
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
<div class="mb-3">
|
||||||
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
||||||
|
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -58,8 +61,9 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="mb-3">compose.yaml</h4>
|
<h4 class="mb-3">compose.yaml</h4>
|
||||||
|
|
||||||
<div class="shadow-box mb-3">
|
<!-- YAML editor -->
|
||||||
<prism-editor v-model="stack.composeYAML" class="yaml-editor" :highlight="highlighter" line-numbers :readonly="!isEditMode" @input="yamlCodeChange"></prism-editor>
|
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
|
||||||
|
<prism-editor ref="editor" v-model="stack.composeYAML" class="yaml-editor" :highlight="highlighter" line-numbers :readonly="!isEditMode" @input="yamlCodeChange"></prism-editor>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ yamlError }}
|
{{ yamlError }}
|
||||||
|
@ -75,6 +79,15 @@
|
||||||
</div>-->
|
</div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!stack.isManagedByDockge">
|
||||||
|
{{ $t("stackNotManagedByDockgeMsg") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Dialog -->
|
||||||
|
<BModal v-model="showDeleteDialog" :okTitle="$t('deleteStack')" okVariant="danger" @ok="deleteDialog">
|
||||||
|
{{ $t("deleteStackMsg") }}
|
||||||
|
</BModal>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -89,6 +102,7 @@ import "prismjs/themes/prism-tomorrow.css";
|
||||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "../../../backend/util-common";
|
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "../../../backend/util-common";
|
||||||
|
import { BModal } from "bootstrap-vue-next";
|
||||||
|
|
||||||
const template = `version: "3.8"
|
const template = `version: "3.8"
|
||||||
services:
|
services:
|
||||||
|
@ -104,6 +118,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
PrismEditor,
|
PrismEditor,
|
||||||
|
BModal,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -112,15 +127,26 @@ export default {
|
||||||
processing: true,
|
processing: true,
|
||||||
showProgressTerminal: false,
|
showProgressTerminal: false,
|
||||||
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
||||||
stack: {},
|
stack: {
|
||||||
|
|
||||||
|
},
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
submitted: false,
|
submitted: false,
|
||||||
|
showDeleteDialog: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isAdd() {
|
isAdd() {
|
||||||
return this.$route.path === "/compose" && !this.submitted;
|
return this.$route.path === "/compose" && !this.submitted;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stack from the global stack list, because it may contain more real-time data like status
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
globalStack() {
|
||||||
|
return this.$root.stackList[this.stack.name];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"stack.composeYAML": {
|
"stack.composeYAML": {
|
||||||
|
@ -139,6 +165,7 @@ export default {
|
||||||
this.stack = {
|
this.stack = {
|
||||||
name: "",
|
name: "",
|
||||||
composeYAML: template,
|
composeYAML: template,
|
||||||
|
isManagedByDockge: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -147,11 +174,17 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
bindTerminal() {
|
||||||
|
// Bind Terminal output
|
||||||
|
const terminalName = getComposeTerminalName(this.stack.name);
|
||||||
|
this.$refs.progressTerminal.bind(terminalName);
|
||||||
|
},
|
||||||
loadStack() {
|
loadStack() {
|
||||||
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
|
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.stack = res.stack;
|
this.stack = res.stack;
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
this.bindTerminal();
|
||||||
} else {
|
} else {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
}
|
}
|
||||||
|
@ -160,9 +193,7 @@ export default {
|
||||||
deployStack() {
|
deployStack() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
// Bind Terminal output
|
this.bindTerminal();
|
||||||
const terminalName = getComposeTerminalName(this.stack.name);
|
|
||||||
this.$refs.progressTerminal.bind(terminalName);
|
|
||||||
|
|
||||||
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
@ -170,8 +201,6 @@ export default {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.$router.push("/compose/" + this.stack.name);
|
this.$router.push("/compose/" + this.stack.name);
|
||||||
} else {
|
|
||||||
this.submitted = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -187,6 +216,14 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deleteDialog() {
|
||||||
|
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
if (res.ok) {
|
||||||
|
this.$router.push("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
discardStack() {
|
discardStack() {
|
||||||
this.loadStack();
|
this.loadStack();
|
||||||
|
@ -221,4 +258,8 @@ export default {
|
||||||
.terminal {
|
.terminal {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-box.edit-mode {
|
||||||
|
background-color: #2c2f38 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Terminal :allow-input="true" class="terminal"></Terminal>
|
<Terminal ref="terminal" :allow-input="true" class="terminal" :rows="20"></Terminal>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -20,7 +20,16 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.terminalFit(50);
|
// Bind Terminal Component to Socket.io
|
||||||
|
const terminalName = "console";
|
||||||
|
this.$refs.terminal.bind(terminalName);
|
||||||
|
|
||||||
|
// Create a new Terminal
|
||||||
|
this.$root.getSocket().emit("mainTerminal", terminalName, (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"check-password-strength": "~2.0.7",
|
"check-password-strength": "~2.0.7",
|
||||||
"command-exists": "^1.2.9",
|
"command-exists": "^1.2.9",
|
||||||
"compare-versions": "~6.1.0",
|
"compare-versions": "~6.1.0",
|
||||||
|
"croner": "^7.0.4",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
||||||
compare-versions:
|
compare-versions:
|
||||||
specifier: ~6.1.0
|
specifier: ~6.1.0
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
|
croner:
|
||||||
|
specifier: ^7.0.4
|
||||||
|
version: 7.0.4
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.10
|
specifier: ^1.11.10
|
||||||
version: 1.11.10
|
version: 1.11.10
|
||||||
|
@ -1783,6 +1786,11 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/croner@7.0.4:
|
||||||
|
resolution: {integrity: sha512-P8Zd88km8oQ0xH8Es0u75GtOnFyCNopuAhlFv5kAnbcTuXd0xNvRTgnxnJEs63FicCOsHTL7rpu4BHzY3cMq4w==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-env@7.0.3:
|
/cross-env@7.0.3:
|
||||||
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
|
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
|
||||||
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||||
|
|
Loading…
Reference in a new issue