diff --git a/README.md b/README.md index 74e0f6a..151d1fd 100644 --- a/README.md +++ b/README.md @@ -9,28 +9,33 @@ A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager. ## ⭐ Features - Focus on `docker-compose.yml` stack management -- Interactive editor for `docker-compose.yml` files +- Interactive editor for `docker-compose.yml` - Interactive web terminal for containers and any docker commands - 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/UX, 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` +- Convert `docker run ...` command into `docker-compose.yml` file ## Installation ## Motivations -- Try ES Module and TypeScript in 2023 -- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, it keeps spinning the loading icon for a few minutes, and I don't know what's going on. +- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. +- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js) + If you love this project, please consider giving this project a ⭐. ## More Ideas? -- Container file manager +- Stats +- File manager - App store for yaml templates -- Container stats - Get app icons - Switch Docker context - Support Dockerfile and build - Zero-config private docker registry - Support Docker swarm + + + diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 1a3e801..e2e7f42 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -424,14 +424,27 @@ export class DockgeServer { } sendStackList(useCache = false) { - let stackList = Stack.getStackList(this, useCache); let roomList = this.io.sockets.adapter.rooms.keys(); + let map : Map | undefined; + for (let room of roomList) { // Check if the room is a number (user id) if (Number(room)) { + + // Get the list only if there is a room + if (!map) { + map = new Map(); + let stackList = Stack.getStackList(this, useCache); + + for (let [ stackName, stack ] of stackList) { + map.set(stackName, stack.toSimpleJSON()); + } + } + + log.debug("server", "Send stack list to room " + room); this.io.to(room).emit("stackList", { ok: true, - stackList: Object.fromEntries(stackList), + stackList: Object.fromEntries(map), }); } } diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts index cdba197..8d791db 100644 --- a/backend/routers/main-router.ts +++ b/backend/routers/main-router.ts @@ -10,6 +10,13 @@ export class MainRouter extends Router { res.send(server.indexHTML); }); + // Robots.txt + router.get("/robots.txt", async (_request, response) => { + let txt = "User-agent: *\nDisallow: /"; + response.setHeader("Content-Type", "text/plain"); + response.send(txt); + }); + return router; } diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index 3fd4dd0..92e4b93 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -14,6 +14,7 @@ export class DockerSocketHandler extends SocketHandler { server.sendStackList(); callback({ ok: true, + msg: "Deployed", }); } catch (e) { callbackError(e, callback); @@ -69,6 +70,9 @@ export class DockerSocketHandler extends SocketHandler { } const stack = Stack.getStack(server, stackName); + + stack.startCombinedTerminal(socket); + callback({ ok: true, stack: stack.toJSON(), @@ -77,6 +81,107 @@ export class DockerSocketHandler extends SocketHandler { callbackError(e, callback); } }); + + // requestStackList + socket.on("requestStackList", async (callback) => { + try { + checkLogin(socket); + server.sendStackList(); + callback({ + ok: true, + msg: "Updated" + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // startStack + socket.on("startStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.start(socket); + callback({ + ok: true, + msg: "Started" + }); + server.sendStackList(); + + stack.startCombinedTerminal(socket); + + } catch (e) { + callbackError(e, callback); + } + }); + + // stopStack + socket.on("stopStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.stop(socket); + callback({ + ok: true, + msg: "Stopped" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + // restartStack + socket.on("restartStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.restart(socket); + callback({ + ok: true, + msg: "Restarted" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + // updateStack + socket.on("updateStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.update(socket); + callback({ + ok: true, + msg: "Updated" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); } saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack { @@ -95,5 +200,6 @@ export class DockerSocketHandler extends SocketHandler { stack.save(isAdd); return stack; } + } diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index 3322799..6162e7a 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -8,64 +8,49 @@ import fs from "fs"; import { allowedCommandList, allowedRawKeys, - getComposeTerminalName, + getComposeTerminalName, getContainerExecTerminalName, isDev, PROGRESS_TERMINAL_ROWS } from "../util-common"; -import { MainTerminal, Terminal } from "../terminal"; +import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal"; export class TerminalSocketHandler extends SocketHandler { create(socket : DockgeSocket, server : DockgeServer) { - socket.on("terminalInputRaw", async (key : unknown) => { + socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => { try { checkLogin(socket); - if (typeof(key) !== "string") { - throw new Error("Key must be a string."); + if (typeof(terminalName) !== "string") { + throw new Error("Terminal name must be a string."); } - if (allowedRawKeys.includes(key)) { - server.terminal.write(key); - } - } catch (e) { - - } - }); - - socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback : unknown) => { - try { - checkLogin(socket); - if (typeof(cmd) !== "string") { throw new Error("Command must be a string."); } - // Check if the command is allowed - const cmdParts = cmd.split(" "); - const executable = cmdParts[0].trim(); - log.debug("console", "Executable: " + executable); - log.debug("console", "Executable length: " + executable.length); - - if (!allowedCommandList.includes(executable)) { - throw new Error("Command not allowed."); + let terminal = Terminal.getTerminal(terminalName); + if (terminal instanceof InteractiveTerminal) { + terminal.write(cmd); + } else { + throw new Error("Terminal not found or it is not a Interactive Terminal."); } - - server.terminal.write(cmd); } catch (e) { - if (typeof(errorCallback) === "function") { - errorCallback({ - ok: false, - msg: e.message, - }); - } + errorCallback({ + ok: false, + msg: e.message, + }); } }); - // Create Terminal + // Main Terminal socket.on("mainTerminal", async (terminalName : unknown, callback) => { try { checkLogin(socket); + + // TODO: Reset the name here, force one main terminal for now + terminalName = "console"; + if (typeof(terminalName) !== "string") { throw new ValidationError("Terminal name must be a string."); } @@ -91,7 +76,40 @@ export class TerminalSocketHandler extends SocketHandler { } }); - // Join Terminal + // Interactive Terminal for containers + socket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string."); + } + + if (typeof(serviceName) !== "string") { + throw new ValidationError("Service name must be a string."); + } + + const terminalName = getContainerExecTerminalName(stackName, serviceName, 0); + let terminal = Terminal.getTerminal(terminalName); + + if (!terminal) { + terminal = new InteractiveTerminal(server, terminalName); + terminal.rows = 50; + log.debug("deployStack", "Terminal created"); + } + + terminal.join(socket); + terminal.start(); + + callback({ + ok: true, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // Join Output Terminal socket.on("terminalJoin", async (terminalName : unknown, callback) => { if (typeof(callback) !== "function") { log.debug("console", "Callback is not a function."); @@ -124,18 +142,9 @@ export class TerminalSocketHandler extends SocketHandler { }); - // Resize Terminal + // TODO: Resize Terminal socket.on("terminalResize", async (rows : unknown) => { - try { - checkLogin(socket); - if (typeof(rows) !== "number") { - throw new Error("Rows must be a number."); - } - log.debug("console", "Resize terminal to " + rows + " rows."); - server.terminal.resize(rows); - } catch (e) { - } }); } } diff --git a/backend/stack.ts b/backend/stack.ts index 04defc5..7db7c4f 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -5,9 +5,11 @@ import yaml from "yaml"; import { DockgeSocket, ValidationError } from "./util-server"; import path from "path"; import { + COMBINED_TERMINAL_COLS, + COMBINED_TERMINAL_ROWS, CREATED_FILE, CREATED_STACK, - EXITED, + EXITED, getCombinedTerminalName, getComposeTerminalName, PROGRESS_TERMINAL_ROWS, RUNNING, @@ -24,6 +26,8 @@ export class Stack { protected _configFilePath?: string; protected server: DockgeServer; + protected combinedTerminal? : Terminal; + protected static managedStackList: Map = new Map(); constructor(server : DockgeServer, name : string, composeYAML? : string) { @@ -37,7 +41,6 @@ export class Stack { return { ...obj, composeYAML: this.composeYAML, - isManagedByDockge: this.isManagedByDockge, }; } @@ -46,9 +49,20 @@ export class Stack { name: this.name, status: this._status, tags: [], + isManagedByDockge: this.isManagedByDockge, }; } + /** + * Get the status of the stack from `docker compose ps --format json` + */ + ps() : object { + let res = childProcess.execSync("docker compose ps --format json", { + cwd: this.path + }); + return JSON.parse(res.toString()); + } + get isManagedByDockge() : boolean { if (this._configFilePath) { return this._configFilePath.startsWith(this.server.stackDirFullPath) && fs.existsSync(this.path) && fs.statSync(this.path).isDirectory(); @@ -128,70 +142,29 @@ export class Stack { fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML); } - deploy(socket? : DockgeSocket) : Promise { + async deploy(socket? : DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - log.debug("deployStack", "Terminal name: " + terminalName); - - const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "up", "-d" ], this.path); - log.debug("deployStack", "Terminal created"); - - terminal.rows = PROGRESS_TERMINAL_ROWS; - - if (socket) { - terminal.join(socket); - log.debug("deployStack", "Terminal joined"); - } else { - log.debug("deployStack", "No socket, not joining"); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to deploy, please check the terminal output for more information."); } - - return new Promise((resolve, reject) => { - terminal.onExit((exitCode : number) => { - if (exitCode === 0) { - resolve(exitCode); - } else { - reject(new Error("Failed to deploy, please check the terminal output for more information.")); - } - }); - terminal.start(); - }); + return exitCode; } - delete(socket?: DockgeSocket) : Promise { - // Docker compose down + async delete(socket?: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - log.debug("deleteStack", "Terminal name: " + terminalName); - - const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "down" ], this.path); - - terminal.rows = PROGRESS_TERMINAL_ROWS; - - if (socket) { - terminal.join(socket); - log.debug("deployStack", "Terminal joined"); - } else { - log.debug("deployStack", "No socket, not joining"); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "down", "--remove-orphans", "--rmi", "all" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to delete, please check the terminal output for more information."); } - 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(); + // Remove the stack folder + fs.rmSync(this.path, { + recursive: true, + force: true }); + return exitCode; } static getStackList(server : DockgeServer, useCacheForManaged = false) : Map { @@ -257,6 +230,10 @@ export class Stack { return statusList; } + /** + * Convert the status string from `docker compose ls` to the status number + * @param status + */ static statusConvert(status : string) : number { if (status.startsWith("created")) { return CREATED_STACK; @@ -290,4 +267,53 @@ export class Stack { stack._configFilePath = path.resolve(dir); return stack; } + + async start(socket: DockgeSocket) { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to start, please check the terminal output for more information."); + } + return exitCode; + } + + async stop(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "stop" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to stop, please check the terminal output for more information."); + } + return exitCode; + } + + async restart(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "restart" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to restart, please check the terminal output for more information."); + } + return exitCode; + } + + async update(socket: DockgeSocket) { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "pull" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to pull, please check the terminal output for more information."); + } + exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to restart, please check the terminal output for more information."); + } + return exitCode; + } + + async startCombinedTerminal(socket: DockgeSocket) { + const terminalName = getCombinedTerminalName(this.name); + const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker-compose", [ "logs", "-f" ], this.path); + terminal.rows = COMBINED_TERMINAL_ROWS; + terminal.cols = COMBINED_TERMINAL_COLS; + terminal.join(socket); + terminal.start(); + } } diff --git a/backend/terminal.ts b/backend/terminal.ts index 278325d..352372e 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -3,7 +3,14 @@ import * as os from "node:os"; import * as pty from "@homebridge/node-pty-prebuilt-multiarch"; import { LimitQueue } from "./utils/limit-queue"; import { DockgeSocket } from "./util-server"; -import { getCryptoRandomInt, TERMINAL_COLS, TERMINAL_ROWS } from "./util-common"; +import { + allowedCommandList, allowedRawKeys, + getComposeTerminalName, + getCryptoRandomInt, + PROGRESS_TERMINAL_ROWS, + TERMINAL_COLS, + TERMINAL_ROWS +} from "./util-common"; import { sync as commandExistsSync } from "command-exists"; import { log } from "./log"; @@ -25,6 +32,7 @@ export class Terminal { protected callback? : (exitCode : number) => void; protected _rows : number = TERMINAL_ROWS; + protected _cols : number = TERMINAL_COLS; constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) { this.server = server; @@ -43,7 +51,16 @@ export class Terminal { set rows(rows : number) { this._rows = rows; - this.ptyProcess?.resize(TERMINAL_COLS, rows); + this.ptyProcess?.resize(this.cols, this.rows); + } + + get cols() { + return this._cols; + } + + set cols(cols : number) { + this._cols = cols; + this.ptyProcess?.resize(this.cols, this.rows); } public start() { @@ -119,6 +136,30 @@ export class Terminal { public static getTerminal(name : string) : Terminal | undefined { return Terminal.terminalMap.get(name); } + + public static getOrCreateTerminal(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) : Terminal { + let terminal = Terminal.getTerminal(name); + if (!terminal) { + terminal = new Terminal(server, name, file, args, cwd); + } + return terminal; + } + + public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise { + const terminal = new Terminal(server, terminalName, file, args, cwd); + terminal.rows = PROGRESS_TERMINAL_ROWS; + + if (socket) { + terminal.join(socket); + } + + return new Promise((resolve) => { + terminal.onExit((exitCode : number) => { + resolve(exitCode); + }); + terminal.start(); + }); + } } /** @@ -140,7 +181,7 @@ export class InteractiveTerminal extends Terminal { * User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir */ export class MainTerminal extends InteractiveTerminal { - constructor(server : DockgeServer, name : string, cwd : string = "./") { + constructor(server : DockgeServer, name : string) { let shell; if (os.platform() === "win32") { @@ -152,6 +193,25 @@ export class MainTerminal extends InteractiveTerminal { } else { shell = "bash"; } - super(server, name, shell, [], cwd); + super(server, name, shell, [], server.stacksDir); + } + + public write(input : string) { + // For like Ctrl + C + if (allowedRawKeys.includes(input)) { + super.write(input); + return; + } + + // Check if the command is allowed + const cmdParts = input.split(" "); + const executable = cmdParts[0].trim(); + log.debug("console", "Executable: " + executable); + log.debug("console", "Executable length: " + executable.length); + + if (!allowedCommandList.includes(executable)) { + throw new Error("Command not allowed."); + } + super.write(input); } } diff --git a/backend/util-common.ts b/backend/util-common.ts index d918d19..637cf8c 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -11,6 +11,8 @@ dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(relativeTime); +import { parseDocument, Document } from "yaml"; + let randomBytes : (numBytes: number) => Uint8Array; if (typeof window !== "undefined" && window.crypto) { @@ -50,13 +52,13 @@ export function statusName(status : number) : string { export function statusNameShort(status : number) : string { switch (status) { case CREATED_FILE: - return "draft"; + return "inactive"; case CREATED_STACK: return "inactive"; case RUNNING: return "active"; case EXITED: - return "inactive"; + return "exited"; default: return "?"; } @@ -67,7 +69,7 @@ export function statusColor(status : number) : string { case CREATED_FILE: return "dark"; case CREATED_STACK: - return "danger"; + return "dark"; case RUNNING: return "primary"; case EXITED: @@ -78,10 +80,13 @@ export function statusColor(status : number) : string { } export const isDev = process.env.NODE_ENV === "development"; -export const TERMINAL_COLS = 80; +export const TERMINAL_COLS = 105; export const TERMINAL_ROWS = 10; export const PROGRESS_TERMINAL_ROWS = 8; +export const COMBINED_TERMINAL_COLS = 50; +export const COMBINED_TERMINAL_ROWS = 15; + export const ERROR_TYPE_VALIDATION = 1; export const allowedCommandList : string[] = [ @@ -182,11 +187,68 @@ export function getComposeTerminalName(stack : string) { return "compose-" + stack; } +export function getCombinedTerminalName(stack : string) { + return "combined-" + stack; +} + export function getContainerTerminalName(container : string) { return "container-" + container; } -export function getContainerExecTerminalName(container : string, index : number) { +export function getContainerExecTerminalName(stackName : string, container : string, index : number) { return "container-exec-" + container + "-" + index; } +export function copyYAMLComments(doc : Document, src : Document) { + doc.comment = src.comment; + doc.commentBefore = src.commentBefore; + + if (doc && doc.contents && src && src.contents) { + // @ts-ignore + copyYAMLCommentsItems(doc.contents.items, src.contents.items); + } +} + +/** + * Copy yaml comments from srcItems to items + * Typescript is super annoying here, so I have to use any here + * TODO: Since comments are belong to the array index, the comments will be lost if the order of the items is changed or removed or added. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function copyYAMLCommentsItems(items : any, srcItems : any) { + if (!items || !srcItems) { + return; + } + + for (let i = 0; i < items.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const item : any = items[i]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const srcItem : any = srcItems[i]; + + if (!srcItem) { + continue; + } + + if (item.key && srcItem.key) { + item.key.comment = srcItem.key.comment; + item.key.commentBefore = srcItem.key.commentBefore; + } + + if (srcItem.comment) { + item.comment = srcItem.comment; + } + + if (item.value && srcItem.value) { + if (typeof item.value === "object" && typeof srcItem.value === "object") { + item.value.comment = srcItem.value.comment; + item.value.commentBefore = srcItem.value.commentBefore; + + if (item.value.items && srcItem.value.items) { + copyYAMLCommentsItems(item.value.items, srcItem.value.items); + } + } + } + } +} diff --git a/extra/templates/nginx-proxy-manager/compose.yaml b/extra/templates/nginx-proxy-manager/compose.yaml new file mode 100644 index 0000000..49d5813 --- /dev/null +++ b/extra/templates/nginx-proxy-manager/compose.yaml @@ -0,0 +1,12 @@ +version: '3.8' +services: + nginx-proxy-manager: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt diff --git a/extra/templates/uptime-kuma/compose.yaml b/extra/templates/uptime-kuma/compose.yaml new file mode 100644 index 0000000..31a7dca --- /dev/null +++ b/extra/templates/uptime-kuma/compose.yaml @@ -0,0 +1,10 @@ +version: '3.8' +services: + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + volumes: + - ./data:/app/data + ports: + - "3001:3001" + restart: always diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 8d67441..6fe98dc 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -7,15 +7,34 @@ export {} declare module 'vue' { export interface GlobalComponents { + About: typeof import('./src/components/settings/About.vue')['default'] + APIKeys: typeof import('./src/components/settings/APIKeys.vue')['default'] + Appearance: typeof import('./src/components/settings/Appearance.vue')['default'] + ArrayInput: typeof import('./src/components/ArrayInput.vue')['default'] + ArrayInputWithOptions: typeof import('./src/components/ArrayInputWithOptions.vue')['default'] + Backup: typeof import('./src/components/settings/Backup.vue')['default'] BButton: typeof import('bootstrap-vue-next')['BButton'] BModal: typeof import('bootstrap-vue-next')['BModal'] Confirm: typeof import('./src/components/Confirm.vue')['default'] + Container: typeof import('./src/components/Container.vue')['default'] + ContainerDialog: typeof import('./src/components/ContainerDialog.vue')['default'] + Docker: typeof import('./src/components/settings/Docker.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'] + MonitorHistory: typeof import('./src/components/settings/MonitorHistory.vue')['default'] + NetworkInput: typeof import('./src/components/NetworkInput.vue')['default'] + Notifications: typeof import('./src/components/settings/Notifications.vue')['default'] + Proxies: typeof import('./src/components/settings/Proxies.vue')['default'] + ReverseProxy: typeof import('./src/components/settings/ReverseProxy.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + Security: typeof import('./src/components/settings/Security.vue')['default'] StackList: typeof import('./src/components/StackList.vue')['default'] StackListItem: typeof import('./src/components/StackListItem.vue')['default'] + Tags: typeof import('./src/components/settings/Tags.vue')['default'] Terminal: typeof import('./src/components/Terminal.vue')['default'] + TwoFADialog: typeof import('./src/components/TwoFADialog.vue')['default'] Uptime: typeof import('./src/components/Uptime.vue')['default'] } } diff --git a/frontend/src/components/ArrayInput.vue b/frontend/src/components/ArrayInput.vue new file mode 100644 index 0000000..52a54bd --- /dev/null +++ b/frontend/src/components/ArrayInput.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue new file mode 100644 index 0000000..affa592 --- /dev/null +++ b/frontend/src/components/Container.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/frontend/src/components/HiddenInput.vue b/frontend/src/components/HiddenInput.vue new file mode 100644 index 0000000..fb86a39 --- /dev/null +++ b/frontend/src/components/HiddenInput.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/NetworkInput.vue b/frontend/src/components/NetworkInput.vue new file mode 100644 index 0000000..2788dc0 --- /dev/null +++ b/frontend/src/components/NetworkInput.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/components/StackList.vue b/frontend/src/components/StackList.vue index fce0652..90edb2f 100644 --- a/frontend/src/components/StackList.vue +++ b/frontend/src/components/StackList.vue @@ -12,14 +12,11 @@ - +
- +
@@ -54,7 +51,6 @@ v-for="(item, index) in sortedStackList" :key="index" :stack="item" - :showPathName="filtersActive" :isSelectMode="selectMode" :isSelected="isSelected" :select="select" diff --git a/frontend/src/components/StackListItem.vue b/frontend/src/components/StackListItem.vue index 4dd8825..ee0a8aa 100644 --- a/frontend/src/components/StackListItem.vue +++ b/frontend/src/components/StackListItem.vue @@ -1,32 +1,8 @@ diff --git a/frontend/src/components/TwoFADialog.vue b/frontend/src/components/TwoFADialog.vue new file mode 100644 index 0000000..6ded47a --- /dev/null +++ b/frontend/src/components/TwoFADialog.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/frontend/src/components/settings/About.vue b/frontend/src/components/settings/About.vue new file mode 100644 index 0000000..39179f7 --- /dev/null +++ b/frontend/src/components/settings/About.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/settings/Appearance.vue b/frontend/src/components/settings/Appearance.vue new file mode 100644 index 0000000..c974b6e --- /dev/null +++ b/frontend/src/components/settings/Appearance.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/components/settings/General.vue b/frontend/src/components/settings/General.vue new file mode 100644 index 0000000..2f1d296 --- /dev/null +++ b/frontend/src/components/settings/General.vue @@ -0,0 +1,117 @@ + + + + diff --git a/frontend/src/components/settings/Security.vue b/frontend/src/components/settings/Security.vue new file mode 100644 index 0000000..2fe4d44 --- /dev/null +++ b/frontend/src/components/settings/Security.vue @@ -0,0 +1,204 @@ + + + diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 64b575c..8799143 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,4 +1,5 @@ -import { createI18n } from "vue-i18n"; +// @ts-ignore Performance issue when using "vue-i18n", so we use "vue-i18n/dist/vue-i18n.esm-browser.prod.js", but typescript doesn't like that. +import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js"; import en from "./lang/en.json"; const languageList = { diff --git a/frontend/src/icon.ts b/frontend/src/icon.ts index 98fcf1c..0599e6a 100644 --- a/frontend/src/icon.ts +++ b/frontend/src/icon.ts @@ -14,6 +14,7 @@ import { faEyeSlash, faList, faPause, + faStop, faPlay, faPlus, faSearch, @@ -51,6 +52,8 @@ import { faClone, faCertificate, faTerminal, faWarehouse, faHome, faRocket, + faRotate, + faCloudArrowDown, faArrowsRotate, } from "@fortawesome/free-solid-svg-icons"; library.add( @@ -61,6 +64,7 @@ library.add( faEyeSlash, faList, faPause, + faStop, faPlay, faPlus, faSearch, @@ -102,6 +106,9 @@ library.add( faWarehouse, faHome, faRocket, + faRotate, + faCloudArrowDown, + faArrowsRotate, ); export { FontAwesomeIcon }; diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 8dcb89d..4ab9421 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -14,11 +14,32 @@ "deleteStack": "Delete", "stopStack": "Stop", "restartStack": "Restart", + "updateStack": "Update", "startStack": "Start", "editStack": "Edit", "discardStack": "Discard", "saveStackDraft": "Save", "notAvailableShort" : "N/A", "deleteStackMsg": "Are you sure you want to delete this stack?", - "stackNotManagedByDockgeMsg": "This stack is not managed by Dockge." + "stackNotManagedByDockgeMsg": "This stack is not managed by Dockge.", + "primaryHostname": "Primary Hostname", + "general": "General", + "container": "Container | Containers", + "scanFolder": "Scan Stacks Folder", + "dockerImage": "Image", + "restartPolicyUnlessStopped": "Unless Stopped", + "restartPolicyAlways": "Always", + "restartPolicyOnFailure": "On Failure", + "restartPolicyNo": "No", + "environmentVariable": "Environment Variable | Environment Variables", + "restartPolicy": "Restart Policy", + "containerName": "Container Name", + "port": "Port | Ports", + "volume": "Volume | Volumes", + "network": "Network | Networks", + "dependsOn": "Container Dependency | Container Dependencies", + "addListItem": "Add {0}", + "deleteContainer": "Delete", + "addContainer": "Add Container", + "addNetwork": "Add Network" } diff --git a/frontend/src/layouts/Layout.vue b/frontend/src/layouts/Layout.vue index 88d5b9d..ac36091 100644 --- a/frontend/src/layouts/Layout.vue +++ b/frontend/src/layouts/Layout.vue @@ -58,7 +58,13 @@ -->
  • - + +
  • + +
  • + {{ $t("Settings") }}
  • @@ -75,42 +81,10 @@ - -
    - - - Dockge - - -
    - - -
    -
    @@ -163,7 +137,11 @@ export default { }, methods: { - + scanFolder() { + this.$root.getSocket().emit("requestStackList", (res) => { + this.$root.toastRes(res); + }); + }, }, }; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index bad068a..1c8defd 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,9 +1,11 @@ +// Dayjs init inside this, so it has to be the first import +await import("../../backend/util-common"); + import { createApp, defineComponent, h } from "vue"; import App from "./App.vue"; import { router } from "./router"; import { FontAwesomeIcon } from "./icon.js"; import { i18n } from "./i18n"; -await import("../../backend/util-common"); // Dependencies import "bootstrap"; @@ -15,12 +17,6 @@ import "vue-toastification/dist/index.css"; import "xterm/css/xterm.css"; import "./styles/main.scss"; -// Dayjs -import dayjs from "dayjs"; -import timezone from "dayjs/plugin/timezone"; -import utc from "dayjs/plugin/utc"; -import relativeTime from "dayjs/plugin/relativeTime"; - // Minxins import socket from "./mixins/socket"; import lang from "./mixins/lang"; @@ -30,18 +26,8 @@ const app = createApp(rootApp()); app.use(Toast, { position: POSITION.BOTTOM_RIGHT, - containerClassName: "toast-container mb-5", showCloseButtonOnHover: true, - - filterBeforeCreate: (toast, toasts) => { - if (toast.timeout === 0) { - return false; - } else { - return toast; - } - }, }); - app.use(router); app.use(i18n); app.component("FontAwesomeIcon", FontAwesomeIcon); diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index cd764fd..7e76565 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -4,20 +4,10 @@ import { defineComponent } from "vue"; import jwtDecode from "jwt-decode"; import { Terminal } from "xterm"; -let terminalInputBuffer = ""; -let cursorPosition = 0; let socket : Socket; let terminalMap : Map = new Map(); -function removeInput() { - const backspaceCount = terminalInputBuffer.length; - const backspaces = "\b \b".repeat(backspaceCount); - cursorPosition = 0; - terminal.write(backspaces); - terminalInputBuffer = ""; -} - export default defineComponent({ data() { return { @@ -38,6 +28,7 @@ export default defineComponent({ allowLoginDialog: false, username: null, stackList: {}, + composeTemplate: "", }; }, computed: { @@ -59,49 +50,7 @@ export default defineComponent({ }, mounted() { return; - terminal.onKey(e => { - const code = e.key.charCodeAt(0); - console.debug("Encode: " + JSON.stringify(e.key)); - if (e.key === "\r") { - // Return if no input - if (terminalInputBuffer.length === 0) { - return; - } - - const buffer = terminalInputBuffer; - - // Remove the input from the terminal - removeInput(); - - socket.emit("terminalInput", buffer + e.key, (err) => { - this.toastError(err.msg); - }); - - } else if (code === 127) { // Backspace - if (cursorPosition > 0) { - terminal.write("\b \b"); - cursorPosition--; - terminalInputBuffer = terminalInputBuffer.slice(0, -1); - } - } else if (e.key === "\u001B\u005B\u0041" || e.key === "\u001B\u005B\u0042") { // UP OR DOWN - // Do nothing - - } else if (e.key === "\u001B\u005B\u0043") { // RIGHT - // TODO - } else if (e.key === "\u001B\u005B\u0044") { // LEFT - // TODO - } else if (e.key === "\u0003") { // Ctrl + C - console.debug("Ctrl + C"); - socket.emit("terminalInputRaw", e.key); - removeInput(); - } else { - cursorPosition++; - terminalInputBuffer += e.key; - console.log(terminalInputBuffer); - terminal.write(e.key); - } - }); }, methods: { /** @@ -206,9 +155,6 @@ export default defineComponent({ socket.on("stackStatusList", (res) => { if (res.ok) { - - console.log(res.stackStatusList); - for (let stackName in res.stackStatusList) { const stackObj = this.stackList[stackName]; if (stackObj) { diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index 21ec6b6..6075a90 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -5,7 +5,7 @@

    {{ stack.name }}

    -
    +
    - - - - - - - + + + + + + + + +
    + + +
    @@ -31,17 +54,18 @@
    -
    +
    +
    -

    General

    +

    {{ $t("general") }}

    @@ -51,24 +75,75 @@
    -

    Containers

    -
    -
    - {{ name }} {{ service }} -
    + +

    {{ $tc("container", 2) }}

    + +
    + + +
    + +
    + +
    + + + + +
    +

    Logs

    +
    -
    +

    compose.yaml

    - +
    -
    +
    {{ yamlError }}
    +

    {{ $tc("network", 2) }}

    +
    + +
    + +

    {{ $tc("volume", 2) }}

    +
    +
    + + , +

    - +
    + + diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue index 1fc2d84..7b653af 100644 --- a/frontend/src/pages/Dashboard.vue +++ b/frontend/src/pages/Dashboard.vue @@ -1,14 +1,14 @@ + + diff --git a/frontend/src/router.ts b/frontend/src/router.ts index e730314..39df4d7 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -6,6 +6,15 @@ import Dashboard from "./pages/Dashboard.vue"; import DashboardHome from "./pages/DashboardHome.vue"; import Console from "./pages/Console.vue"; import Compose from "./pages/Compose.vue"; +import ContainerTerminal from "./pages/ContainerTerminal.vue"; + +const Settings = () => import("./pages/Settings.vue"); + +// Settings - Sub Pages +import Appearance from "./components/settings/Appearance.vue"; +import General from "./components/settings/General.vue"; +const Security = () => import("./components/settings/Security.vue"); +import About from "./components/settings/About.vue"; const routes = [ { @@ -30,14 +39,42 @@ const routes = [ name: "compose", component: Compose, props: true, + children: [ + { + path: "/compose/:stackName/terminal/:serviceName/:type", + component: ContainerTerminal, + name: "containerTerminal", + }, + ] }, - ] }, { path: "/console", component: Console, }, + { + path: "/settings", + component: Settings, + children: [ + { + path: "general", + component: General, + }, + { + path: "appearance", + component: Appearance, + }, + { + path: "security", + component: Security, + }, + { + path: "about", + component: About, + }, + ] + }, ] }, ] diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index 0823482..e69cac3 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -35,6 +35,10 @@ textarea.form-control { color: $maintenance !important; } +::placeholder { + color: $dark-font-color3 !important; +} + .incident a, .bg-maintenance a { color: inherit; @@ -45,7 +49,7 @@ textarea.form-control { .dark & { .list-group-item { - background-color: $dark-bg; + background-color: $dark-bg2; color: $dark-font-color; border-color: $dark-border-color; } @@ -320,6 +324,7 @@ optgroup { .bg-primary { color: $dark-font-color2; + background: $primary-gradient; } .btn-secondary { @@ -486,21 +491,19 @@ optgroup { } .item { - display: block; + display: flex; + align-items: center; + height: 52px; text-decoration: none; - padding: 13px 15px 10px 15px; border-radius: 10px; transition: all ease-in-out 0.15s; + width: 100%; + padding: 0 8px; &.disabled { opacity: 0.3; } - .info { - white-space: nowrap; - overflow: hidden; - } - &:hover { background-color: $highlight-white; } @@ -508,9 +511,10 @@ optgroup { &.active { background-color: #cdf8f4; } - .tags { - // Removes margin to line up tags list with uptime percentage - margin-left: -0.25rem; + + .title { + display: inline-block; + margin-top: -4px; } } } @@ -676,5 +680,18 @@ code { } } +// Vue Prism Editor bug - workaround +// https://github.com/koca/vue-prism-editor/issues/87 +/* +.prism-editor__textarea { + width: 999999px !important; +} +.prism-editor__editor { + white-space: pre !important; +} +.prism-editor__container { + overflow-x: scroll !important; +}*/ + // Localization @import "localization.scss"; diff --git a/frontend/src/util-frontend.ts b/frontend/src/util-frontend.ts index 2b3f9ba..e55ebf0 100644 --- a/frontend/src/util-frontend.ts +++ b/frontend/src/util-frontend.ts @@ -212,3 +212,4 @@ export function getToastErrorTimeout() { return errorTimeout; } + diff --git a/package.json b/package.json index 9377c75..521341a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "check-password-strength": "~2.0.7", "command-exists": "^1.2.9", "compare-versions": "~6.1.0", + "composerize": "^1.4.1", "croner": "^7.0.4", "dayjs": "^1.11.10", "express": "~4.18.2", @@ -46,6 +47,7 @@ "@fortawesome/free-regular-svg-icons": "6.4.2", "@fortawesome/free-solid-svg-icons": "6.4.2", "@fortawesome/vue-fontawesome": "3.0.3", + "@types/bootstrap": "^5.2.8", "@types/command-exists": "~1.2.2", "@types/express": "~4.17.20", "@types/jsonwebtoken": "~9.0.4", @@ -70,6 +72,7 @@ "vue-eslint-parser": "^9.3.2", "vue-i18n": "~9.5.0", "vue-prism-editor": "~2.0.0-alpha.2", + "vue-qrcode": "^2.2.0", "vue-router": "~4.2.5", "vue-toastification": "~2.0.0-rc.5", "xterm": "~5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9665f6a..7c93895 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: compare-versions: specifier: ~6.1.0 version: 6.1.0 + composerize: + specifier: ^1.4.1 + version: 1.4.1 croner: specifier: ^7.0.4 version: 7.0.4 @@ -93,6 +96,9 @@ devDependencies: '@fortawesome/vue-fontawesome': specifier: 3.0.3 version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.6) + '@types/bootstrap': + specifier: ^5.2.8 + version: 5.2.8 '@types/command-exists': specifier: ~1.2.2 version: 1.2.2 @@ -165,6 +171,9 @@ devDependencies: vue-prism-editor: specifier: ~2.0.0-alpha.2 version: 2.0.0-alpha.2(vue@3.3.6) + vue-qrcode: + specifier: ^2.2.0 + version: 2.2.0(qrcode@1.5.3)(vue@3.3.6) vue-router: specifier: ~4.2.5 version: 4.2.5(vue@3.3.6) @@ -809,6 +818,12 @@ packages: '@types/node': 20.8.7 dev: true + /@types/bootstrap@5.2.8: + resolution: {integrity: sha512-14do+aWZPc1w3G+YevSsy8eas1XEPhTOUNBhQX/r12YKn7ySssATJusBQ/HCQAd2nq54U8vvrftHSb1YpeJUXg==} + dependencies: + '@popperjs/core': 2.11.8 + dev: true + /@types/command-exists@1.2.2: resolution: {integrity: sha512-1qKPTkjLmghE5C7UUHXGcLaG8MNftchOcLAIryUXNKahRO5beS+iJ9rIL8XD4+B8K2phjYUsPQDox1FRX4KMTQ==} dev: true @@ -1330,7 +1345,6 @@ packages: dependencies: sprintf-js: 1.0.3 dev: false - optional: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1578,6 +1592,10 @@ packages: engines: {node: '>=6'} dev: true + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1653,6 +1671,14 @@ packages: dev: false optional: true + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1730,6 +1756,17 @@ packages: resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} dev: false + /composerize@1.4.1: + resolution: {integrity: sha512-63bKZMgOE8Fd6jBSmkZFdOtKM5A46TafrWgNHVXIBfTu/YBBZw91xkjALG3JiuOatX4tMK04uM37O4HGypnkfQ==} + hasBin: true + dependencies: + core-js: 2.6.12 + deepmerge: 2.2.1 + invariant: 2.2.4 + yamljs: 0.3.0 + yargs-parser: 13.1.2 + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1772,6 +1809,12 @@ packages: engines: {node: '>= 0.6'} dev: false + /core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1875,6 +1918,10 @@ packages: dependencies: ms: 2.1.2 + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1891,6 +1938,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -1935,6 +1987,10 @@ packages: engines: {node: '>=8'} dev: false + /dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1974,12 +2030,15 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} requiresBuild: true - dev: false /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: false + /encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + dev: true + /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -2417,6 +2476,14 @@ packages: array-back: 3.1.0 dev: false + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2552,6 +2619,11 @@ packages: is-property: 1.0.2 dev: false + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: @@ -2861,6 +2933,12 @@ packages: engines: {node: '>= 0.10'} dev: false + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + /ip@1.1.8: resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} requiresBuild: true @@ -2904,7 +2982,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} requiresBuild: true - dev: false /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -2955,6 +3032,10 @@ packages: dev: false optional: true + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -3170,6 +3251,13 @@ packages: engines: {node: '>=14'} dev: true + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3227,6 +3315,13 @@ packages: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} dev: false + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + /lru-cache@10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} @@ -3706,6 +3801,13 @@ packages: type-check: 0.4.0 dev: true + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3713,6 +3815,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -3729,6 +3838,11 @@ packages: dev: false optional: true + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + /pac-proxy-agent@7.0.1: resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} engines: {node: '>= 14'} @@ -3949,6 +4063,11 @@ packages: dev: false optional: true + /pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + dev: true + /postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} @@ -4074,6 +4193,17 @@ packages: engines: {node: '>=6'} dev: true + /qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + dev: true + /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -4166,6 +4296,11 @@ packages: engines: {node: '>=6'} dev: false + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /require-in-the-middle@5.2.0: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} @@ -4179,6 +4314,10 @@ packages: dev: false optional: true + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4321,7 +4460,6 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: false /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} @@ -4516,7 +4654,6 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} requiresBuild: true dev: false - optional: true /sprintf-js@1.1.2: resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} @@ -4554,7 +4691,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: false /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} @@ -4729,8 +4865,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} requiresBuild: true - dev: false - optional: true /tsx@3.14.0: resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} @@ -5045,6 +5179,17 @@ packages: vue: 3.3.6(typescript@5.2.2) dev: true + /vue-qrcode@2.2.0(qrcode@1.5.3)(vue@3.3.6): + resolution: {integrity: sha512-pEwy/IznxEY5MXptFLaxbGdeDWIJRgU5VhBcFmg1avDjD2z2jjWAGE5dlDwqagXtUjcgkvFSSQ40boog1maLuw==} + peerDependencies: + qrcode: ^1.5.0 + vue: ^2.7.0 || ^3.0.0 + dependencies: + qrcode: 1.5.3 + tslib: 2.6.2 + vue: 3.3.6(typescript@5.2.2) + dev: true + /vue-router@4.2.5(vue@3.3.6): resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} peerDependencies: @@ -5098,6 +5243,10 @@ packages: webidl-conversions: 3.0.1 dev: false + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5119,6 +5268,15 @@ packages: typical: 5.2.0 dev: false + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5206,6 +5364,10 @@ packages: resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} dev: true + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -5221,7 +5383,38 @@ packages: argparse: 1.0.10 glob: 7.2.3 dev: false - optional: true + + /yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: true /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}