mirror of
https://github.com/louislam/dockge.git
synced 2025-01-21 05:41:15 +01:00
wip
This commit is contained in:
parent
e67d08b7b3
commit
314630724b
17
README.md
17
README.md
@ -9,28 +9,33 @@ A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.
|
|||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
- Focus on `docker-compose.yml` stack management
|
- 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
|
- 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/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`
|
- 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
|
## Installation
|
||||||
|
|
||||||
## Motivations
|
## 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, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear.
|
||||||
- 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.
|
- 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 ⭐.
|
If you love this project, please consider giving this project a ⭐.
|
||||||
|
|
||||||
## More Ideas?
|
## More Ideas?
|
||||||
|
|
||||||
- Container file manager
|
- Stats
|
||||||
|
- File manager
|
||||||
- App store for yaml templates
|
- App store for yaml templates
|
||||||
- Container stats
|
|
||||||
- Get app icons
|
- Get app icons
|
||||||
- Switch Docker context
|
- Switch Docker context
|
||||||
- Support Dockerfile and build
|
- Support Dockerfile and build
|
||||||
- Zero-config private docker registry
|
- Zero-config private docker registry
|
||||||
- Support Docker swarm
|
- Support Docker swarm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -424,14 +424,27 @@ export class DockgeServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendStackList(useCache = false) {
|
sendStackList(useCache = false) {
|
||||||
let stackList = Stack.getStackList(this, useCache);
|
|
||||||
let roomList = this.io.sockets.adapter.rooms.keys();
|
let roomList = this.io.sockets.adapter.rooms.keys();
|
||||||
|
let map : Map<string, object> | undefined;
|
||||||
|
|
||||||
for (let room of roomList) {
|
for (let room of roomList) {
|
||||||
// Check if the room is a number (user id)
|
// Check if the room is a number (user id)
|
||||||
if (Number(room)) {
|
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", {
|
this.io.to(room).emit("stackList", {
|
||||||
ok: true,
|
ok: true,
|
||||||
stackList: Object.fromEntries(stackList),
|
stackList: Object.fromEntries(map),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,13 @@ export class MainRouter extends Router {
|
|||||||
res.send(server.indexHTML);
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
server.sendStackList();
|
server.sendStackList();
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
msg: "Deployed",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callbackError(e, callback);
|
callbackError(e, callback);
|
||||||
@ -69,6 +70,9 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = Stack.getStack(server, stackName);
|
||||||
|
|
||||||
|
stack.startCombinedTerminal(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
stack: stack.toJSON(),
|
stack: stack.toJSON(),
|
||||||
@ -77,6 +81,107 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
callbackError(e, callback);
|
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 {
|
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {
|
||||||
@ -95,5 +200,6 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
stack.save(isAdd);
|
stack.save(isAdd);
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,64 +8,49 @@ import fs from "fs";
|
|||||||
import {
|
import {
|
||||||
allowedCommandList,
|
allowedCommandList,
|
||||||
allowedRawKeys,
|
allowedRawKeys,
|
||||||
getComposeTerminalName,
|
getComposeTerminalName, getContainerExecTerminalName,
|
||||||
isDev,
|
isDev,
|
||||||
PROGRESS_TERMINAL_ROWS
|
PROGRESS_TERMINAL_ROWS
|
||||||
} from "../util-common";
|
} from "../util-common";
|
||||||
import { MainTerminal, Terminal } from "../terminal";
|
import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal";
|
||||||
|
|
||||||
export class TerminalSocketHandler extends SocketHandler {
|
export class TerminalSocketHandler extends SocketHandler {
|
||||||
create(socket : DockgeSocket, server : DockgeServer) {
|
create(socket : DockgeSocket, server : DockgeServer) {
|
||||||
|
|
||||||
socket.on("terminalInputRaw", async (key : unknown) => {
|
socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
if (typeof(key) !== "string") {
|
if (typeof(terminalName) !== "string") {
|
||||||
throw new Error("Key must be a 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") {
|
if (typeof(cmd) !== "string") {
|
||||||
throw new Error("Command must be a string.");
|
throw new Error("Command must be a string.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the command is allowed
|
let terminal = Terminal.getTerminal(terminalName);
|
||||||
const cmdParts = cmd.split(" ");
|
if (terminal instanceof InteractiveTerminal) {
|
||||||
const executable = cmdParts[0].trim();
|
terminal.write(cmd);
|
||||||
log.debug("console", "Executable: " + executable);
|
} else {
|
||||||
log.debug("console", "Executable length: " + executable.length);
|
throw new Error("Terminal not found or it is not a Interactive Terminal.");
|
||||||
|
|
||||||
if (!allowedCommandList.includes(executable)) {
|
|
||||||
throw new Error("Command not allowed.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.terminal.write(cmd);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (typeof(errorCallback) === "function") {
|
errorCallback({
|
||||||
errorCallback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Terminal
|
// Main Terminal
|
||||||
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
|
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
|
// TODO: Reset the name here, force one main terminal for now
|
||||||
|
terminalName = "console";
|
||||||
|
|
||||||
if (typeof(terminalName) !== "string") {
|
if (typeof(terminalName) !== "string") {
|
||||||
throw new ValidationError("Terminal name must be a 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) => {
|
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.");
|
||||||
@ -124,18 +142,9 @@ export class TerminalSocketHandler extends SocketHandler {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize Terminal
|
// TODO: Resize Terminal
|
||||||
socket.on("terminalResize", async (rows : unknown) => {
|
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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
140
backend/stack.ts
140
backend/stack.ts
@ -5,9 +5,11 @@ 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 {
|
import {
|
||||||
|
COMBINED_TERMINAL_COLS,
|
||||||
|
COMBINED_TERMINAL_ROWS,
|
||||||
CREATED_FILE,
|
CREATED_FILE,
|
||||||
CREATED_STACK,
|
CREATED_STACK,
|
||||||
EXITED,
|
EXITED, getCombinedTerminalName,
|
||||||
getComposeTerminalName,
|
getComposeTerminalName,
|
||||||
PROGRESS_TERMINAL_ROWS,
|
PROGRESS_TERMINAL_ROWS,
|
||||||
RUNNING,
|
RUNNING,
|
||||||
@ -24,6 +26,8 @@ export class Stack {
|
|||||||
protected _configFilePath?: string;
|
protected _configFilePath?: string;
|
||||||
protected server: DockgeServer;
|
protected server: DockgeServer;
|
||||||
|
|
||||||
|
protected combinedTerminal? : Terminal;
|
||||||
|
|
||||||
protected static managedStackList: Map<string, Stack> = new Map();
|
protected static managedStackList: Map<string, Stack> = new Map();
|
||||||
|
|
||||||
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
||||||
@ -37,7 +41,6 @@ export class Stack {
|
|||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
composeYAML: this.composeYAML,
|
composeYAML: this.composeYAML,
|
||||||
isManagedByDockge: this.isManagedByDockge,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +49,20 @@ export class Stack {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
status: this._status,
|
status: this._status,
|
||||||
tags: [],
|
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 {
|
get isManagedByDockge() : boolean {
|
||||||
if (this._configFilePath) {
|
if (this._configFilePath) {
|
||||||
return this._configFilePath.startsWith(this.server.stackDirFullPath) && fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
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);
|
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy(socket? : DockgeSocket) : Promise<number> {
|
async deploy(socket? : DockgeSocket) : Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(this.name);
|
const terminalName = getComposeTerminalName(this.name);
|
||||||
log.debug("deployStack", "Terminal name: " + terminalName);
|
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path);
|
||||||
|
if (exitCode !== 0) {
|
||||||
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "up", "-d" ], this.path);
|
throw new Error("Failed to deploy, please check the terminal output for more information.");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
return exitCode;
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(socket?: DockgeSocket) : Promise<number> {
|
async delete(socket?: DockgeSocket) : Promise<number> {
|
||||||
// Docker compose down
|
|
||||||
const terminalName = getComposeTerminalName(this.name);
|
const terminalName = getComposeTerminalName(this.name);
|
||||||
log.debug("deleteStack", "Terminal name: " + terminalName);
|
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "down", "--remove-orphans", "--rmi", "all" ], this.path);
|
||||||
|
if (exitCode !== 0) {
|
||||||
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "down" ], this.path);
|
throw new Error("Failed to delete, please check the terminal output for more information.");
|
||||||
|
|
||||||
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
|
||||||
|
|
||||||
if (socket) {
|
|
||||||
terminal.join(socket);
|
|
||||||
log.debug("deployStack", "Terminal joined");
|
|
||||||
} else {
|
|
||||||
log.debug("deployStack", "No socket, not joining");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// Remove the stack folder
|
||||||
terminal.onExit((exitCode : number) => {
|
fs.rmSync(this.path, {
|
||||||
if (exitCode === 0) {
|
recursive: true,
|
||||||
// Remove the stack folder
|
force: true
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
|
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
|
||||||
@ -257,6 +230,10 @@ export class Stack {
|
|||||||
return statusList;
|
return statusList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the status string from `docker compose ls` to the status number
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
static statusConvert(status : string) : number {
|
static statusConvert(status : string) : number {
|
||||||
if (status.startsWith("created")) {
|
if (status.startsWith("created")) {
|
||||||
return CREATED_STACK;
|
return CREATED_STACK;
|
||||||
@ -290,4 +267,53 @@ export class Stack {
|
|||||||
stack._configFilePath = path.resolve(dir);
|
stack._configFilePath = path.resolve(dir);
|
||||||
return stack;
|
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<number> {
|
||||||
|
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<number> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,14 @@ import * as os from "node:os";
|
|||||||
import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
|
import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
|
||||||
import { LimitQueue } from "./utils/limit-queue";
|
import { LimitQueue } from "./utils/limit-queue";
|
||||||
import { DockgeSocket } from "./util-server";
|
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 { sync as commandExistsSync } from "command-exists";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
|
|
||||||
@ -25,6 +32,7 @@ export class Terminal {
|
|||||||
protected callback? : (exitCode : number) => void;
|
protected callback? : (exitCode : number) => void;
|
||||||
|
|
||||||
protected _rows : number = TERMINAL_ROWS;
|
protected _rows : number = TERMINAL_ROWS;
|
||||||
|
protected _cols : number = TERMINAL_COLS;
|
||||||
|
|
||||||
constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
|
constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
@ -43,7 +51,16 @@ export class Terminal {
|
|||||||
|
|
||||||
set rows(rows : number) {
|
set rows(rows : number) {
|
||||||
this._rows = rows;
|
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() {
|
public start() {
|
||||||
@ -119,6 +136,30 @@ export class Terminal {
|
|||||||
public static getTerminal(name : string) : Terminal | undefined {
|
public static getTerminal(name : string) : Terminal | undefined {
|
||||||
return Terminal.terminalMap.get(name);
|
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<number> {
|
||||||
|
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
|
* User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir
|
||||||
*/
|
*/
|
||||||
export class MainTerminal extends InteractiveTerminal {
|
export class MainTerminal extends InteractiveTerminal {
|
||||||
constructor(server : DockgeServer, name : string, cwd : string = "./") {
|
constructor(server : DockgeServer, name : string) {
|
||||||
let shell;
|
let shell;
|
||||||
|
|
||||||
if (os.platform() === "win32") {
|
if (os.platform() === "win32") {
|
||||||
@ -152,6 +193,25 @@ export class MainTerminal extends InteractiveTerminal {
|
|||||||
} else {
|
} else {
|
||||||
shell = "bash";
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ dayjs.extend(utc);
|
|||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
import { parseDocument, Document } from "yaml";
|
||||||
|
|
||||||
let randomBytes : (numBytes: number) => Uint8Array;
|
let randomBytes : (numBytes: number) => Uint8Array;
|
||||||
|
|
||||||
if (typeof window !== "undefined" && window.crypto) {
|
if (typeof window !== "undefined" && window.crypto) {
|
||||||
@ -50,13 +52,13 @@ export function statusName(status : number) : string {
|
|||||||
export function statusNameShort(status : number) : string {
|
export function statusNameShort(status : number) : string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CREATED_FILE:
|
case CREATED_FILE:
|
||||||
return "draft";
|
return "inactive";
|
||||||
case CREATED_STACK:
|
case CREATED_STACK:
|
||||||
return "inactive";
|
return "inactive";
|
||||||
case RUNNING:
|
case RUNNING:
|
||||||
return "active";
|
return "active";
|
||||||
case EXITED:
|
case EXITED:
|
||||||
return "inactive";
|
return "exited";
|
||||||
default:
|
default:
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
@ -67,7 +69,7 @@ export function statusColor(status : number) : string {
|
|||||||
case CREATED_FILE:
|
case CREATED_FILE:
|
||||||
return "dark";
|
return "dark";
|
||||||
case CREATED_STACK:
|
case CREATED_STACK:
|
||||||
return "danger";
|
return "dark";
|
||||||
case RUNNING:
|
case RUNNING:
|
||||||
return "primary";
|
return "primary";
|
||||||
case EXITED:
|
case EXITED:
|
||||||
@ -78,10 +80,13 @@ export function statusColor(status : number) : string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = 105;
|
||||||
export const TERMINAL_ROWS = 10;
|
export const TERMINAL_ROWS = 10;
|
||||||
export const PROGRESS_TERMINAL_ROWS = 8;
|
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 ERROR_TYPE_VALIDATION = 1;
|
||||||
|
|
||||||
export const allowedCommandList : string[] = [
|
export const allowedCommandList : string[] = [
|
||||||
@ -182,11 +187,68 @@ export function getComposeTerminalName(stack : string) {
|
|||||||
return "compose-" + stack;
|
return "compose-" + stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCombinedTerminalName(stack : string) {
|
||||||
|
return "combined-" + stack;
|
||||||
|
}
|
||||||
|
|
||||||
export function getContainerTerminalName(container : string) {
|
export function getContainerTerminalName(container : string) {
|
||||||
return "container-" + container;
|
return "container-" + container;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContainerExecTerminalName(container : string, index : number) {
|
export function getContainerExecTerminalName(stackName : string, container : string, index : number) {
|
||||||
return "container-exec-" + container + "-" + index;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12
extra/templates/nginx-proxy-manager/compose.yaml
Normal file
12
extra/templates/nginx-proxy-manager/compose.yaml
Normal file
@ -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
|
10
extra/templates/uptime-kuma/compose.yaml
Normal file
10
extra/templates/uptime-kuma/compose.yaml
Normal file
@ -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
|
19
frontend/components.d.ts
vendored
19
frontend/components.d.ts
vendored
@ -7,15 +7,34 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
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']
|
BButton: typeof import('bootstrap-vue-next')['BButton']
|
||||||
BModal: typeof import('bootstrap-vue-next')['BModal']
|
BModal: typeof import('bootstrap-vue-next')['BModal']
|
||||||
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
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']
|
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']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
Security: typeof import('./src/components/settings/Security.vue')['default']
|
||||||
StackList: typeof import('./src/components/StackList.vue')['default']
|
StackList: typeof import('./src/components/StackList.vue')['default']
|
||||||
StackListItem: typeof import('./src/components/StackListItem.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']
|
Terminal: typeof import('./src/components/Terminal.vue')['default']
|
||||||
|
TwoFADialog: typeof import('./src/components/TwoFADialog.vue')['default']
|
||||||
Uptime: typeof import('./src/components/Uptime.vue')['default']
|
Uptime: typeof import('./src/components/Uptime.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
frontend/src/components/ArrayInput.vue
Normal file
97
frontend/src/components/ArrayInput.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul v-if="isArrayInited" class="list-group">
|
||||||
|
<li v-for="(value, index) in array" :key="index" class="list-group-item">
|
||||||
|
<input v-model="array[index]" type="text" class="no-bg domain-input" :placeholder="placeholder" />
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-normal btn-sm mt-3" @click="addField">{{ $t("addListItem", [ displayName ]) }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
array() {
|
||||||
|
// Create the array if not exists, it should be safe.
|
||||||
|
if (!this.service[this.name]) {
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
this.service[this.name] = [];
|
||||||
|
}
|
||||||
|
return this.service[this.name];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the array is inited before called v-for.
|
||||||
|
* Prevent empty arrays inserted to the YAML file.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isArrayInited() {
|
||||||
|
return this.service[this.name] !== undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
service() {
|
||||||
|
return this.$parent.$parent.service;
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addField() {
|
||||||
|
this.array.push("");
|
||||||
|
},
|
||||||
|
remove(index) {
|
||||||
|
this.array.splice(index, 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../styles/vars.scss";
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
|
||||||
|
.domain-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border: none;
|
||||||
|
color: $dark-font-color;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #1d2634;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
231
frontend/src/components/Container.vue
Normal file
231
frontend/src/components/Container.vue
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<template>
|
||||||
|
<div class="shadow-box big-padding mb-3 container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7">
|
||||||
|
<h4>{{ name }}</h4>
|
||||||
|
<div class="image mb-2">
|
||||||
|
<span class="me-1">{{ imageName }}:</span><span class="tag">{{ imageTag }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isEditMode">
|
||||||
|
<span class="badge bg-primary me-1">Running</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-5">
|
||||||
|
<div class="function">
|
||||||
|
<router-link v-if="!isEditMode" class="btn btn-normal" :to="terminalRouteLink">
|
||||||
|
<font-awesome-icon icon="terminal" />
|
||||||
|
Terminal
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isEditMode" class="mt-2">
|
||||||
|
<button class="btn btn-normal me-2" @click="showConfig = !showConfig">
|
||||||
|
<font-awesome-icon icon="edit" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button v-if="false" class="btn btn-normal me-2">Rename</button>
|
||||||
|
<button class="btn btn-danger me-2" @click="remove">
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
{{ $t("deleteContainer") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div v-if="isEditMode && showConfig" class="config mt-3">
|
||||||
|
<!-- Image -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("dockerImage") }}
|
||||||
|
</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
v-model="service.image"
|
||||||
|
class="form-control"
|
||||||
|
list="image-datalist"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: Search online: https://hub.docker.com/api/content/v1/products/search?q=louislam%2Fuptime&source=community&page=1&page_size=4 -->
|
||||||
|
<datalist id="image-datalist">
|
||||||
|
<option value="louislam/uptime-kuma:1" />
|
||||||
|
</datalist>
|
||||||
|
<div class="form-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ports -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $tc("port", 2) }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="ports" :display-name="$t('port')" placeholder="HOST:CONTAINER" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Volumes -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $tc("volume", 2) }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="volumes" :display-name="$t('volume')" placeholder="HOST:CONTAINER" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Restart Policy -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("restartPolicy") }}
|
||||||
|
</label>
|
||||||
|
<select v-model="service.restart" class="form-select">
|
||||||
|
<option value="always">{{ $t("restartPolicyAlways") }}</option>
|
||||||
|
<option value="unless-stopped">{{ $t("restartPolicyUnlessStopped") }}</option>
|
||||||
|
<option value="on-failure">{{ $t("restartPolicyOnFailure") }}</option>
|
||||||
|
<option value="no">{{ $t("restartPolicyNo") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Environment Variables -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $tc("environmentVariable", 2) }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="environment" :display-name="$t('environmentVariable')" placeholder="KEY=VALUE" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container Name -->
|
||||||
|
<div v-if="false" class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("containerName") }}
|
||||||
|
</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
v-model="service.container_name"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $tc("network", 2) }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="networks" :display-name="$t('network')" placeholder="Network Name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Depends on -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $t("dependsOn") }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="depends_on" :display-name="$t('dependsOn')" placeholder="Container Name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isEditMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
first: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showConfig: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
terminalRouteLink() {
|
||||||
|
return {
|
||||||
|
name: "containerTerminal",
|
||||||
|
params: {
|
||||||
|
serviceName: this.name,
|
||||||
|
type: "logs",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
service() {
|
||||||
|
return this.jsonObject.services[this.name];
|
||||||
|
},
|
||||||
|
|
||||||
|
jsonObject() {
|
||||||
|
return this.$parent.$parent.jsonConfig;
|
||||||
|
},
|
||||||
|
imageName() {
|
||||||
|
if (this.service.image) {
|
||||||
|
return this.service.image.split(":")[0];
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageTag() {
|
||||||
|
if (this.service.image) {
|
||||||
|
let tag = this.service.image.split(":")[1];
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
return tag;
|
||||||
|
} else {
|
||||||
|
return "latest";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.first) {
|
||||||
|
//this.showConfig = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
remove() {
|
||||||
|
delete this.jsonObject.services[this.name];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "../styles/vars";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
.image {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6c757d;
|
||||||
|
.tag {
|
||||||
|
color: #33383b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function {
|
||||||
|
align-content: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
87
frontend/src/components/HiddenInput.vue
Normal file
87
frontend/src/components/HiddenInput.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="model"
|
||||||
|
:type="visibility"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
>
|
||||||
|
|
||||||
|
<a v-if="visibility == 'password'" class="btn btn-outline-primary" @click="showInput()">
|
||||||
|
<font-awesome-icon icon="eye" />
|
||||||
|
</a>
|
||||||
|
<a v-if="visibility == 'text'" class="btn btn-outline-primary" @click="hideInput()">
|
||||||
|
<font-awesome-icon icon="eye-slash" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/** The value of the input */
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/** A placeholder to use */
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
/** Maximum length of the input */
|
||||||
|
maxlength: {
|
||||||
|
type: Number,
|
||||||
|
default: 255
|
||||||
|
},
|
||||||
|
/** Should the field auto complete */
|
||||||
|
autocomplete: {
|
||||||
|
type: String,
|
||||||
|
default: "new-password",
|
||||||
|
},
|
||||||
|
/** Is the input required? */
|
||||||
|
required: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
/** Should the input be read only? */
|
||||||
|
readonly: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visibility: "password",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** Show users input in plain text */
|
||||||
|
showInput() {
|
||||||
|
this.visibility = "text";
|
||||||
|
},
|
||||||
|
/** Censor users input */
|
||||||
|
hideInput() {
|
||||||
|
this.visibility = "password";
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
106
frontend/src/components/NetworkInput.vue
Normal file
106
frontend/src/components/NetworkInput.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h5>Internal Networks</h5>
|
||||||
|
|
||||||
|
<ul class="list-group">
|
||||||
|
<li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item">
|
||||||
|
<input v-model="networkList[index].key" type="text" class="no-bg domain-input" placeholder="Network name..." />
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-normal btn-sm mt-3" @click="addField">{{ $t("addNetwork") }}</button>
|
||||||
|
|
||||||
|
<h5 class="mt-3">External Networks</h5>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
networkList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
isInited() {
|
||||||
|
return this.networks !== undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
networks() {
|
||||||
|
return this.$parent.$parent.networks;
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
networks: {
|
||||||
|
handler() {
|
||||||
|
this.loadNetworkList();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
networkList: {
|
||||||
|
handler() {
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadNetworkList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
loadNetworkList() {
|
||||||
|
console.debug("loadNetworkList", this.networks);
|
||||||
|
|
||||||
|
this.networkList = [];
|
||||||
|
for (const key in this.networks) {
|
||||||
|
this.networkList.push({
|
||||||
|
key: key,
|
||||||
|
value: this.networks[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addField() {
|
||||||
|
this.networkList.push({
|
||||||
|
key: "",
|
||||||
|
value: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove(index) {
|
||||||
|
this.networkList.splice(index, 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../styles/vars.scss";
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
|
||||||
|
.domain-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border: none;
|
||||||
|
color: $dark-font-color;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #1d2634;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -12,14 +12,11 @@
|
|||||||
<a v-if="searchText == ''" class="search-icon">
|
<a v-if="searchText == ''" class="search-icon">
|
||||||
<font-awesome-icon icon="search" />
|
<font-awesome-icon icon="search" />
|
||||||
</a>
|
</a>
|
||||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
<a v-if="searchText != ''" class="search-icon" style="cursor: pointer" @click="clearSearchText">
|
||||||
<font-awesome-icon icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</a>
|
</a>
|
||||||
<form>
|
<form>
|
||||||
<input
|
<input v-model="searchText" class="form-control search-input" autocomplete="off" />
|
||||||
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +51,6 @@
|
|||||||
v-for="(item, index) in sortedStackList"
|
v-for="(item, index) in sortedStackList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:stack="item"
|
:stack="item"
|
||||||
:showPathName="filtersActive"
|
|
||||||
:isSelectMode="selectMode"
|
:isSelectMode="selectMode"
|
||||||
:isSelected="isSelected"
|
:isSelected="isSelected"
|
||||||
:select="select"
|
:select="select"
|
||||||
|
@ -1,32 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{ 'dim' : !stack.isManagedByDockge }">
|
<router-link :to="`/compose/${stack.name}`" :class="{ 'dim' : !stack.isManagedByDockge }" class="item">
|
||||||
<div :style="depthMargin">
|
<Uptime :stack="stack" :fixed-width="true" class="me-2" />
|
||||||
<!-- Checkbox -->
|
<span class="title">{{ stackName }}</span>
|
||||||
<div v-if="isSelectMode" class="select-input-wrapper">
|
</router-link>
|
||||||
<input
|
|
||||||
class="form-check-input select-input"
|
|
||||||
type="checkbox"
|
|
||||||
:aria-label="$t('Check/Uncheck')"
|
|
||||||
:checked="isSelected(stack.id)"
|
|
||||||
@click.stop="toggleSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link :to="`/compose/${stack.name}`" class="item">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-9 col-md-8 small-padding">
|
|
||||||
<div class="info">
|
|
||||||
<Uptime :stack="stack" :fixed-width="true" />
|
|
||||||
{{ stackName }}
|
|
||||||
</div>
|
|
||||||
<div v-if="stack.tags.length > 0" class="tags">
|
|
||||||
<!--<Tag v-for="tag in stack.tags" :key="tag" :item="tag" :size="'sm'" />-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -43,11 +19,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
/** Should the stack name show it's parent */
|
|
||||||
showPathName: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
/** If the user is in select mode */
|
/** If the user is in select mode */
|
||||||
isSelectMode: {
|
isSelectMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -86,11 +57,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
stackName() {
|
stackName() {
|
||||||
if (this.showPathName) {
|
return this.stack.name;
|
||||||
return this.stack.pathName;
|
|
||||||
} else {
|
|
||||||
return this.stack.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -18,34 +18,62 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
allowInput: {
|
name: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
default: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
rows: {
|
rows: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: TERMINAL_ROWS,
|
default: TERMINAL_ROWS,
|
||||||
|
},
|
||||||
|
|
||||||
|
cols: {
|
||||||
|
type: Number,
|
||||||
|
default: TERMINAL_COLS,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mode
|
||||||
|
// displayOnly: Only display terminal output
|
||||||
|
// mainTerminal: Allow input limited commands and output
|
||||||
|
// interactive: Free input and output
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: "displayOnly",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "has-data" ],
|
emits: [ "has-data" ],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: null,
|
|
||||||
first: true,
|
first: true,
|
||||||
|
terminalInputBuffer: "",
|
||||||
|
cursorPosition: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
let cursorBlink = true;
|
||||||
|
|
||||||
|
if (this.mode === "displayOnly") {
|
||||||
|
cursorBlink = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.terminal = new Terminal({
|
this.terminal = new Terminal({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
cursorBlink: this.allowInput,
|
cursorBlink,
|
||||||
cols: TERMINAL_COLS,
|
cols: this.cols,
|
||||||
rows: this.rows,
|
rows: this.rows,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.mode === "mainTerminal") {
|
||||||
|
this.mainTerminalConfig();
|
||||||
|
} else if (this.mode === "interactive") {
|
||||||
|
this.interactiveTerminalConfig();
|
||||||
|
}
|
||||||
|
|
||||||
//this.terminal.loadAddon(new WebLinksAddon());
|
//this.terminal.loadAddon(new WebLinksAddon());
|
||||||
|
|
||||||
// Bind to a div
|
// Bind to a div
|
||||||
@ -60,6 +88,24 @@ export default {
|
|||||||
this.first = false;
|
this.first = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.bind();
|
||||||
|
|
||||||
|
// Create a new Terminal
|
||||||
|
if (this.mode === "mainTerminal") {
|
||||||
|
this.$root.getSocket().emit("mainTerminal", this.name, (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (this.mode === "interactive") {
|
||||||
|
this.$root.getSocket().emit("interactiveTerminal", this.name, (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
unmounted() {
|
||||||
@ -69,12 +115,77 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
bind(name) {
|
bind(name) {
|
||||||
if (this.name) {
|
// Workaround: normally this.name should be set, but it is not sometimes, so we use the parameter, but eventually this.name and name must be the same name
|
||||||
|
if (name) {
|
||||||
|
this.$root.unbindTerminal(name);
|
||||||
|
this.$root.bindTerminal(name, this.terminal);
|
||||||
|
console.debug("Terminal bound via parameter: " + name);
|
||||||
|
} else if (this.name) {
|
||||||
this.$root.unbindTerminal(this.name);
|
this.$root.unbindTerminal(this.name);
|
||||||
|
this.$root.bindTerminal(this.name, this.terminal);
|
||||||
|
console.debug("Terminal bound: " + this.name);
|
||||||
|
} else {
|
||||||
|
console.debug("Terminal name not set");
|
||||||
}
|
}
|
||||||
this.name = name;
|
|
||||||
this.$root.bindTerminal(this.name, this.terminal);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeInput() {
|
||||||
|
const backspaceCount = this.terminalInputBuffer.length;
|
||||||
|
const backspaces = "\b \b".repeat(backspaceCount);
|
||||||
|
this.cursorPosition = 0;
|
||||||
|
this.terminal.write(backspaces);
|
||||||
|
this.terminalInputBuffer = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
mainTerminalConfig() {
|
||||||
|
this.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 (this.terminalInputBuffer.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = this.terminalInputBuffer;
|
||||||
|
|
||||||
|
// Remove the input from the terminal
|
||||||
|
this.removeInput();
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("terminalInput", this.name, buffer + e.key, (err) => {
|
||||||
|
this.$root.toastError(err.msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (code === 127) { // Backspace
|
||||||
|
if (this.cursorPosition > 0) {
|
||||||
|
this.terminal.write("\b \b");
|
||||||
|
this.cursorPosition--;
|
||||||
|
this.terminalInputBuffer = this.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");
|
||||||
|
this.$root.getSocket().emit("terminalInput", this.name, e.key);
|
||||||
|
this.removeInput();
|
||||||
|
} else {
|
||||||
|
this.cursorPosition++;
|
||||||
|
this.terminalInputBuffer += e.key;
|
||||||
|
console.log(this.terminalInputBuffer);
|
||||||
|
this.terminal.write(e.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
interactiveTerminalConfig() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
203
frontend/src/components/TwoFADialog.vue
Normal file
203
frontend/src/components/TwoFADialog.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ $t("Setup 2FA") }}
|
||||||
|
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
|
||||||
|
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
|
||||||
|
</h5>
|
||||||
|
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
|
||||||
|
<vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
|
||||||
|
<button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||||
|
|
||||||
|
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
|
||||||
|
<label for="current-password" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||||
|
{{ $t("Enable 2FA") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
|
||||||
|
{{ $t("Disable 2FA") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="mt-3">
|
||||||
|
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input v-model="token" type="text" maxlength="6" class="form-control" autocomplete="one-time-code" required>
|
||||||
|
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
|
||||||
|
</div>
|
||||||
|
<p v-show="tokenValid" class="mt-2" style="color: green;">{{ $t("tokenValidSettingsMsg") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
|
||||||
|
{{ $t("confirmEnableTwoFAMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
|
||||||
|
{{ $t("confirmDisableTwoFAMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
import VueQrcode from "vue-qrcode";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
VueQrcode,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPassword: "",
|
||||||
|
processing: false,
|
||||||
|
uri: null,
|
||||||
|
tokenValid: false,
|
||||||
|
twoFAStatus: null,
|
||||||
|
token: null,
|
||||||
|
showURI: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
this.getStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** Show the dialog */
|
||||||
|
show() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm enabling 2FA */
|
||||||
|
confirmEnableTwoFA() {
|
||||||
|
this.$refs.confirmEnableTwoFA.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Show dialog to confirm disabling 2FA */
|
||||||
|
confirmDisableTwoFA() {
|
||||||
|
this.$refs.confirmDisableTwoFA.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Prepare 2FA configuration */
|
||||||
|
prepare2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.uri = res.uri;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Save the current 2FA configuration */
|
||||||
|
save2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
|
this.modal.hide();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Disable 2FA for this user */
|
||||||
|
disable2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.getStatus();
|
||||||
|
this.currentPassword = "";
|
||||||
|
this.modal.hide();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Verify the token generated by the user */
|
||||||
|
verifyToken() {
|
||||||
|
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.tokenValid = res.valid;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Get current status of 2FA */
|
||||||
|
getStatus() {
|
||||||
|
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.twoFAStatus = res.status;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../styles/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
66
frontend/src/components/settings/About.vue
Normal file
66
frontend/src/components/settings/About.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
|
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
|
<div class="fs-4 fw-bold">Dockge</div>
|
||||||
|
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
||||||
|
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
|
||||||
|
|
||||||
|
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
|
||||||
|
⚠️ {{ $t("Frontend Version do not match backend version!") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logo {
|
||||||
|
margin: 4em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-link {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontend-version {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #cccccc;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
94
frontend/src/components/settings/Appearance.vue
Normal file
94
frontend/src/components/settings/Appearance.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="my-4">
|
||||||
|
<label for="language" class="form-label">
|
||||||
|
{{ $t("Language") }}
|
||||||
|
</label>
|
||||||
|
<select id="language" v-model="$root.language" class="form-select">
|
||||||
|
<option
|
||||||
|
v-for="(lang, i) in $i18n.availableLocales"
|
||||||
|
:key="`Lang${i}`"
|
||||||
|
:value="lang"
|
||||||
|
>
|
||||||
|
{{ $i18n.messages[lang].languageName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-show="false" class="my-4">
|
||||||
|
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Basic checkbox toggle button group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="btncheck1"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="light"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1">
|
||||||
|
{{ $t("Light") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck2"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="dark"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2">
|
||||||
|
{{ $t("Dark") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="btncheck3"
|
||||||
|
v-model="$root.userTheme"
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="theme"
|
||||||
|
autocomplete="off"
|
||||||
|
value="auto"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3">
|
||||||
|
{{ $t("Auto") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../styles/vars.scss";
|
||||||
|
|
||||||
|
.btn-check:active + .btn-outline-primary,
|
||||||
|
.btn-check:checked + .btn-outline-primary,
|
||||||
|
.btn-check:hover + .btn-outline-primary {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
117
frontend/src/components/settings/General.vue
Normal file
117
frontend/src/components/settings/General.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="false">
|
||||||
|
<form class="my-4" autocomplete="off" @submit.prevent="saveGeneral">
|
||||||
|
<!-- Client side Timezone -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="timezone" class="form-label">
|
||||||
|
{{ $t("Display Timezone") }}
|
||||||
|
</label>
|
||||||
|
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||||
|
<option value="auto">
|
||||||
|
{{ $t("Auto") }}: {{ guessTimezone }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(timezone, index) in timezoneList"
|
||||||
|
:key="index"
|
||||||
|
:value="timezone.value"
|
||||||
|
>
|
||||||
|
{{ timezone.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Timezone -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="timezone" class="form-label">
|
||||||
|
{{ $t("Server Timezone") }}
|
||||||
|
</label>
|
||||||
|
<select id="timezone" v-model="settings.serverTimezone" class="form-select">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option
|
||||||
|
v-for="(timezone, index) in timezoneList"
|
||||||
|
:key="index"
|
||||||
|
:value="timezone.value"
|
||||||
|
>
|
||||||
|
{{ timezone.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Hostname -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="primaryBaseURL">
|
||||||
|
{{ $t("primaryHostname") }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
v-model="settings.primaryHostname"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="localhost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../../components/HiddenInput.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { timezoneList } from "../../util-frontend";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timezoneList: timezoneList(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
guessTimezone() {
|
||||||
|
return dayjs.tz.guess();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/** Save the settings */
|
||||||
|
saveGeneral() {
|
||||||
|
localStorage.timezone = this.$root.userTimezone;
|
||||||
|
this.saveSettings();
|
||||||
|
},
|
||||||
|
/** Get the base URL of the application */
|
||||||
|
autoGetPrimaryBaseURL() {
|
||||||
|
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||||
|
},
|
||||||
|
|
||||||
|
testChrome() {
|
||||||
|
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
204
frontend/src/components/settings/Security.vue
Normal file
204
frontend/src/components/settings/Security.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="settingsLoaded" class="my-4">
|
||||||
|
<!-- Change Password -->
|
||||||
|
<template v-if="!settings.disableAuth">
|
||||||
|
<p>
|
||||||
|
{{ $t("Current User") }}: <strong>{{ $root.username }}</strong>
|
||||||
|
<button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5>
|
||||||
|
<form class="mb-3" @submit.prevent="savePassword">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current-password" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
v-model="password.currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new-password" class="form-label">
|
||||||
|
{{ $t("New Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="new-password"
|
||||||
|
v-model="password.newPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="repeat-new-password" class="form-label">
|
||||||
|
{{ $t("Repeat New Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="repeat-new-password"
|
||||||
|
v-model="password.repeatNewPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': invalidPassword }"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("passwordNotMatchMsg") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
{{ $t("Update Password") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||||
|
<h5 class="my-4 settings-subheading">
|
||||||
|
{{ $t("Two Factor Authentication") }}
|
||||||
|
</h5>
|
||||||
|
<div class="mb-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary me-2"
|
||||||
|
type="button"
|
||||||
|
@click="$refs.TwoFADialog.show()"
|
||||||
|
>
|
||||||
|
{{ $t("2FA Settings") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- Advanced -->
|
||||||
|
<h5 class="my-4 settings-subheading">{{ $t("Advanced") }}</h5>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||||
|
<button v-if="! settings.disableAuth" id="disableAuth-btn" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TwoFADialog ref="TwoFADialog" />
|
||||||
|
|
||||||
|
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<p v-html="$t('disableauth.message1')"></p>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<p v-html="$t('disableauth.message2')"></p>
|
||||||
|
<p>{{ $t("Please use this option carefully!") }}</p>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current-password2" class="form-label">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="current-password2"
|
||||||
|
v-model="password.currentPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Confirm from "../../components/Confirm.vue";
|
||||||
|
import TwoFADialog from "../../components/TwoFADialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
TwoFADialog
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
invalidPassword: false,
|
||||||
|
password: {
|
||||||
|
currentPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
repeatNewPassword: "",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
"password.repeatNewPassword"() {
|
||||||
|
this.invalidPassword = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/** Check new passwords match before saving them */
|
||||||
|
savePassword() {
|
||||||
|
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||||
|
this.invalidPassword = true;
|
||||||
|
} else {
|
||||||
|
this.$root
|
||||||
|
.getSocket()
|
||||||
|
.emit("changePassword", this.password, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
if (res.ok) {
|
||||||
|
this.password.currentPassword = "";
|
||||||
|
this.password.newPassword = "";
|
||||||
|
this.password.repeatNewPassword = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Disable authentication for web app access */
|
||||||
|
disableAuth() {
|
||||||
|
this.settings.disableAuth = true;
|
||||||
|
|
||||||
|
// Need current password to disable auth
|
||||||
|
// Set it to empty if done
|
||||||
|
this.saveSettings(() => {
|
||||||
|
this.password.currentPassword = "";
|
||||||
|
this.$root.username = null;
|
||||||
|
this.$root.socket.token = "autoLogin";
|
||||||
|
}, this.password.currentPassword);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Enable authentication for web app access */
|
||||||
|
enableAuth() {
|
||||||
|
this.settings.disableAuth = false;
|
||||||
|
this.saveSettings();
|
||||||
|
this.$root.storage().removeItem("token");
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Show confirmation dialog for disable auth */
|
||||||
|
confirmDisableAuth() {
|
||||||
|
this.$refs.confirmDisableAuth.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -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";
|
import en from "./lang/en.json";
|
||||||
|
|
||||||
const languageList = {
|
const languageList = {
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faList,
|
faList,
|
||||||
faPause,
|
faPause,
|
||||||
|
faStop,
|
||||||
faPlay,
|
faPlay,
|
||||||
faPlus,
|
faPlus,
|
||||||
faSearch,
|
faSearch,
|
||||||
@ -51,6 +52,8 @@ import {
|
|||||||
faClone,
|
faClone,
|
||||||
faCertificate,
|
faCertificate,
|
||||||
faTerminal, faWarehouse, faHome, faRocket,
|
faTerminal, faWarehouse, faHome, faRocket,
|
||||||
|
faRotate,
|
||||||
|
faCloudArrowDown, faArrowsRotate,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -61,6 +64,7 @@ library.add(
|
|||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faList,
|
faList,
|
||||||
faPause,
|
faPause,
|
||||||
|
faStop,
|
||||||
faPlay,
|
faPlay,
|
||||||
faPlus,
|
faPlus,
|
||||||
faSearch,
|
faSearch,
|
||||||
@ -102,6 +106,9 @@ library.add(
|
|||||||
faWarehouse,
|
faWarehouse,
|
||||||
faHome,
|
faHome,
|
||||||
faRocket,
|
faRocket,
|
||||||
|
faRotate,
|
||||||
|
faCloudArrowDown,
|
||||||
|
faArrowsRotate,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
@ -14,11 +14,32 @@
|
|||||||
"deleteStack": "Delete",
|
"deleteStack": "Delete",
|
||||||
"stopStack": "Stop",
|
"stopStack": "Stop",
|
||||||
"restartStack": "Restart",
|
"restartStack": "Restart",
|
||||||
|
"updateStack": "Update",
|
||||||
"startStack": "Start",
|
"startStack": "Start",
|
||||||
"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?",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,13 @@
|
|||||||
</li>-->
|
</li>-->
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
<button class="dropdown-item" @click="scanFolder">
|
||||||
|
<font-awesome-icon icon="arrows-rotate" /> {{ $t("scanFolder") }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<router-link to="/settings/appearance" class="dropdown-item" :class="{ active: $route.path.includes('settings') }">
|
||||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
@ -75,42 +81,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mobile header -->
|
|
||||||
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
|
||||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
|
||||||
<object class="bi" width="40" height="40" data="/icon.svg" />
|
|
||||||
<span class="fs-4 title ms-2">Dockge</span>
|
|
||||||
</router-link>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<router-view v-if="$root.loggedIn" />
|
<router-view v-if="$root.loggedIn" />
|
||||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Mobile Only -->
|
|
||||||
<div v-if="$root.isMobile" style="width: 100%; height: calc(60px + env(safe-area-inset-bottom));" />
|
|
||||||
<nav v-if="$root.isMobile && $root.loggedIn" class="bottom-nav">
|
|
||||||
<router-link to="/dashboard" class="nav-link">
|
|
||||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
|
||||||
{{ $t("Home") }}
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/list" class="nav-link">
|
|
||||||
<div><font-awesome-icon icon="list" /></div>
|
|
||||||
{{ $t("List") }}
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/add" class="nav-link">
|
|
||||||
<div><font-awesome-icon icon="plus" /></div>
|
|
||||||
{{ $t("Add") }}
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/settings" class="nav-link">
|
|
||||||
<div><font-awesome-icon icon="cog" /></div>
|
|
||||||
{{ $t("Settings") }}
|
|
||||||
</router-link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -163,7 +137,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
scanFolder() {
|
||||||
|
this.$root.getSocket().emit("requestStackList", (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -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 { createApp, defineComponent, h } from "vue";
|
||||||
import App from "./App.vue";
|
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";
|
||||||
@ -15,12 +17,6 @@ import "vue-toastification/dist/index.css";
|
|||||||
import "xterm/css/xterm.css";
|
import "xterm/css/xterm.css";
|
||||||
import "./styles/main.scss";
|
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
|
// Minxins
|
||||||
import socket from "./mixins/socket";
|
import socket from "./mixins/socket";
|
||||||
import lang from "./mixins/lang";
|
import lang from "./mixins/lang";
|
||||||
@ -30,18 +26,8 @@ const app = createApp(rootApp());
|
|||||||
|
|
||||||
app.use(Toast, {
|
app.use(Toast, {
|
||||||
position: POSITION.BOTTOM_RIGHT,
|
position: POSITION.BOTTOM_RIGHT,
|
||||||
containerClassName: "toast-container mb-5",
|
|
||||||
showCloseButtonOnHover: true,
|
showCloseButtonOnHover: true,
|
||||||
|
|
||||||
filterBeforeCreate: (toast, toasts) => {
|
|
||||||
if (toast.timeout === 0) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return toast;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||||
|
@ -4,20 +4,10 @@ import { defineComponent } from "vue";
|
|||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { Terminal } from "xterm";
|
import { Terminal } from "xterm";
|
||||||
|
|
||||||
let terminalInputBuffer = "";
|
|
||||||
let cursorPosition = 0;
|
|
||||||
let socket : Socket;
|
let socket : Socket;
|
||||||
|
|
||||||
let terminalMap : Map<string, Terminal> = new Map();
|
let terminalMap : Map<string, Terminal> = new Map();
|
||||||
|
|
||||||
function removeInput() {
|
|
||||||
const backspaceCount = terminalInputBuffer.length;
|
|
||||||
const backspaces = "\b \b".repeat(backspaceCount);
|
|
||||||
cursorPosition = 0;
|
|
||||||
terminal.write(backspaces);
|
|
||||||
terminalInputBuffer = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -38,6 +28,7 @@ export default defineComponent({
|
|||||||
allowLoginDialog: false,
|
allowLoginDialog: false,
|
||||||
username: null,
|
username: null,
|
||||||
stackList: {},
|
stackList: {},
|
||||||
|
composeTemplate: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -59,49 +50,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
return;
|
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: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
@ -206,9 +155,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
socket.on("stackStatusList", (res) => {
|
socket.on("stackStatusList", (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
||||||
console.log(res.stackStatusList);
|
|
||||||
|
|
||||||
for (let stackName in res.stackStatusList) {
|
for (let stackName in res.stackStatusList) {
|
||||||
const stackObj = this.stackList[stackName];
|
const stackObj = this.stackList[stackName];
|
||||||
if (stackObj) {
|
if (stackObj) {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<h1 v-else class="mb-3"><Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}</h1>
|
<h1 v-else class="mb-3"><Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}</h1>
|
||||||
|
|
||||||
<div v-if="stack.isManagedByDockge" class="mb-3">
|
<div v-if="stack.isManagedByDockge" class="mb-3">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group me-2" 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" />
|
||||||
{{ $t("deployStack") }}
|
{{ $t("deployStack") }}
|
||||||
@ -16,14 +16,37 @@
|
|||||||
{{ $t("saveStackDraft") }}
|
{{ $t("saveStackDraft") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
|
<button v-if="!isEditMode" class="btn btn-secondary" :disabled="processing" @click="enableEditMode">
|
||||||
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
<font-awesome-icon icon="pen" class="me-1" />
|
||||||
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("updateStack") }}</button>
|
{{ $t("editStack") }}
|
||||||
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
|
</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 && !active" class="btn btn-primary" :disabled="processing" @click="startStack">
|
||||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing" @click="showDeleteDialog = !showDeleteDialog">{{ $t("deleteStack") }}</button>
|
<font-awesome-icon icon="play" class="me-1" />
|
||||||
|
{{ $t("startStack") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!isEditMode && active" class="btn btn-normal " :disabled="processing" @click="restartStack">
|
||||||
|
<font-awesome-icon icon="rotate" class="me-1" />
|
||||||
|
{{ $t("restartStack") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="updateStack">
|
||||||
|
<font-awesome-icon icon="cloud-arrow-down" class="me-1" />
|
||||||
|
{{ $t("updateStack") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!isEditMode && active" class="btn btn-normal" :disabled="processing" @click="stopStack">
|
||||||
|
<font-awesome-icon icon="stop" class="me-1" />
|
||||||
|
{{ $t("stopStack") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
||||||
|
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing" @click="showDeleteDialog = !showDeleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" class="me-1" />
|
||||||
|
{{ $t("deleteStack") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress Terminal -->
|
<!-- Progress Terminal -->
|
||||||
@ -31,17 +54,18 @@
|
|||||||
<Terminal
|
<Terminal
|
||||||
v-show="showProgressTerminal"
|
v-show="showProgressTerminal"
|
||||||
ref="progressTerminal"
|
ref="progressTerminal"
|
||||||
:allow-input="false"
|
|
||||||
class="mb-3 terminal"
|
class="mb-3 terminal"
|
||||||
|
:name="terminalName"
|
||||||
:rows="progressTerminalRows"
|
:rows="progressTerminalRows"
|
||||||
@has-data="showProgressTerminal = true; submitted = true;"
|
@has-data="showProgressTerminal = true; submitted = true;"
|
||||||
></Terminal>
|
></Terminal>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div v-if="stack.isManagedByDockge" class="row">
|
<div v-if="stack.isManagedByDockge" class="row">
|
||||||
<div class="col">
|
<div class="col-lg-6">
|
||||||
|
<!-- General -->
|
||||||
<div v-if="isAdd">
|
<div v-if="isAdd">
|
||||||
<h4 class="mb-3">General</h4>
|
<h4 class="mb-3">{{ $t("general") }}</h4>
|
||||||
<div class="shadow-box big-padding mb-3">
|
<div class="shadow-box big-padding mb-3">
|
||||||
<!-- Stack Name -->
|
<!-- Stack Name -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -51,24 +75,75 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="mb-3">Containers</h4>
|
<!-- Containers -->
|
||||||
<div class="shadow-box big-padding mb-3">
|
<h4 class="mb-3">{{ $tc("container", 2) }}</h4>
|
||||||
<div v-for="(service, name) in jsonConfig.services" :key="name">
|
|
||||||
{{ name }} {{ service }}
|
<div v-if="isEditMode" class="input-group mb-3">
|
||||||
</div>
|
<input
|
||||||
|
v-model="newContainerName"
|
||||||
|
placeholder="New Container Name..."
|
||||||
|
class="form-control"
|
||||||
|
@keyup.enter="addContainer"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" @click="addContainer">
|
||||||
|
{{ $t("addContainer") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="containerList">
|
||||||
|
<Container
|
||||||
|
v-for="(service, name) in jsonConfig.services"
|
||||||
|
:key="name"
|
||||||
|
:name="name"
|
||||||
|
:is-edit-mode="isEditMode"
|
||||||
|
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="false && isEditMode && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
||||||
|
|
||||||
|
<!-- Combined Terminal Output -->
|
||||||
|
<div v-show="!isEditMode">
|
||||||
|
<h4 class="mb-3">Logs</h4>
|
||||||
|
<Terminal
|
||||||
|
ref="combinedTerminal"
|
||||||
|
class="mb-3 terminal"
|
||||||
|
:name="combinedTerminalName"
|
||||||
|
:rows="combinedTerminalRows"
|
||||||
|
:cols="combinedTerminalCols"
|
||||||
|
style="height: 350px;"
|
||||||
|
></Terminal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-lg-6">
|
||||||
<h4 class="mb-3">compose.yaml</h4>
|
<h4 class="mb-3">compose.yaml</h4>
|
||||||
|
|
||||||
<!-- YAML editor -->
|
<!-- YAML editor -->
|
||||||
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
|
<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>
|
<prism-editor
|
||||||
|
ref="editor"
|
||||||
|
v-model="stack.composeYAML"
|
||||||
|
class="yaml-editor"
|
||||||
|
:highlight="highlighter"
|
||||||
|
line-numbers :readonly="!isEditMode"
|
||||||
|
@input="yamlCodeChange"
|
||||||
|
@focus="editorFocus = true"
|
||||||
|
@blur="editorFocus = false"
|
||||||
|
></prism-editor>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div v-if="isEditMode" class="mb-3">
|
||||||
{{ yamlError }}
|
{{ yamlError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mb-3">{{ $tc("network", 2) }}</h4>
|
||||||
|
<div class="shadow-box big-padding mb-3">
|
||||||
|
<NetworkInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mb-3">{{ $tc("volume", 2) }}</h4>
|
||||||
|
<div class="shadow-box big-padding mb-3">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- <div class="shadow-box big-padding mb-3">
|
<!-- <div class="shadow-box big-padding mb-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label"> Search Templates</label>
|
<label for="name" class="form-label"> Search Templates</label>
|
||||||
@ -80,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!stack.isManagedByDockge">
|
<div v-if="!stack.isManagedByDockge && !processing">
|
||||||
{{ $t("stackNotManagedByDockgeMsg") }}
|
{{ $t("stackNotManagedByDockgeMsg") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -96,43 +171,65 @@
|
|||||||
import { highlight, languages } from "prismjs/components/prism-core";
|
import { highlight, languages } from "prismjs/components/prism-core";
|
||||||
import { PrismEditor } from "vue-prism-editor";
|
import { PrismEditor } from "vue-prism-editor";
|
||||||
import "prismjs/components/prism-yaml";
|
import "prismjs/components/prism-yaml";
|
||||||
import * as yaml from "yaml";
|
import { parseDocument, Document } from "yaml";
|
||||||
|
|
||||||
import "prismjs/themes/prism-tomorrow.css";
|
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 {
|
||||||
|
COMBINED_TERMINAL_COLS,
|
||||||
|
COMBINED_TERMINAL_ROWS,
|
||||||
|
copyYAMLComments,
|
||||||
|
getCombinedTerminalName,
|
||||||
|
getComposeTerminalName,
|
||||||
|
PROGRESS_TERMINAL_ROWS,
|
||||||
|
RUNNING
|
||||||
|
} from "../../../backend/util-common";
|
||||||
import { BModal } from "bootstrap-vue-next";
|
import { BModal } from "bootstrap-vue-next";
|
||||||
|
import NetworkInput from "../components/NetworkInput.vue";
|
||||||
|
|
||||||
const template = `version: "3.8"
|
const template = `version: "3.8"
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:80"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let yamlErrorTimeout = null;
|
let yamlErrorTimeout = null;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
NetworkInput,
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
PrismEditor,
|
PrismEditor,
|
||||||
BModal,
|
BModal,
|
||||||
},
|
},
|
||||||
|
beforeRouteUpdate(to, from, next) {
|
||||||
|
this.exitConfirm(next);
|
||||||
|
},
|
||||||
|
beforeRouteLeave(to, from, next) {
|
||||||
|
this.exitConfirm(next);
|
||||||
|
},
|
||||||
|
yamlDoc: null, // For keeping the yaml comments
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
editorFocus: false,
|
||||||
jsonConfig: {},
|
jsonConfig: {},
|
||||||
yamlError: "",
|
yamlError: "",
|
||||||
processing: true,
|
processing: true,
|
||||||
showProgressTerminal: false,
|
showProgressTerminal: false,
|
||||||
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
||||||
|
combinedTerminalRows: COMBINED_TERMINAL_ROWS,
|
||||||
|
combinedTerminalCols: COMBINED_TERMINAL_COLS,
|
||||||
stack: {
|
stack: {
|
||||||
|
|
||||||
},
|
},
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
submitted: false,
|
submitted: false,
|
||||||
showDeleteDialog: false,
|
showDeleteDialog: false,
|
||||||
|
newContainerName: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -147,11 +244,59 @@ export default {
|
|||||||
globalStack() {
|
globalStack() {
|
||||||
return this.$root.stackList[this.stack.name];
|
return this.$root.stackList[this.stack.name];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
status() {
|
||||||
|
return this.globalStack?.status;
|
||||||
|
},
|
||||||
|
|
||||||
|
active() {
|
||||||
|
return this.status === RUNNING;
|
||||||
|
},
|
||||||
|
|
||||||
|
terminalName() {
|
||||||
|
if (!this.stack.name) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return getComposeTerminalName(this.stack.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
combinedTerminalName() {
|
||||||
|
if (!this.stack.name) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return getCombinedTerminalName(this.stack.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
networks() {
|
||||||
|
return this.jsonConfig.networks;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"stack.composeYAML": {
|
"stack.composeYAML": {
|
||||||
handler() {
|
handler() {
|
||||||
this.yamlCodeChange();
|
if (this.editorFocus) {
|
||||||
|
console.debug("yaml code changed");
|
||||||
|
this.yamlCodeChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
jsonConfig: {
|
||||||
|
handler() {
|
||||||
|
if (!this.editorFocus) {
|
||||||
|
console.debug("jsonConfig changed");
|
||||||
|
|
||||||
|
let doc = new Document(this.jsonConfig);
|
||||||
|
|
||||||
|
// Stick back the yaml comments
|
||||||
|
if (this.yamlDoc) {
|
||||||
|
copyYAMLComments(doc, this.yamlDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stack.composeYAML = doc.toString();
|
||||||
|
this.yamlDoc = doc;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
@ -161,28 +306,53 @@ export default {
|
|||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
|
|
||||||
|
let composeYAML;
|
||||||
|
|
||||||
|
if (this.$root.composeTemplate) {
|
||||||
|
composeYAML = this.$root.composeTemplate;
|
||||||
|
this.$root.composeTemplate = "";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
composeYAML = template;
|
||||||
|
}
|
||||||
|
|
||||||
// Default Values
|
// Default Values
|
||||||
this.stack = {
|
this.stack = {
|
||||||
name: "",
|
name: "",
|
||||||
composeYAML: template,
|
composeYAML,
|
||||||
isManagedByDockge: true,
|
isManagedByDockge: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.yamlCodeChange();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.stack.name = this.$route.params.stackName;
|
this.stack.name = this.$route.params.stackName;
|
||||||
this.loadStack();
|
this.loadStack();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
bindTerminal() {
|
exitConfirm(next) {
|
||||||
// Bind Terminal output
|
if (this.isEditMode) {
|
||||||
const terminalName = getComposeTerminalName(this.stack.name);
|
if (confirm("You are currently editing a stack. Are you sure you want to leave?")) {
|
||||||
this.$refs.progressTerminal.bind(terminalName);
|
next();
|
||||||
|
} else {
|
||||||
|
next(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bindTerminal() {
|
||||||
|
this.$refs.progressTerminal?.bind(this.terminalName);
|
||||||
|
},
|
||||||
|
|
||||||
loadStack() {
|
loadStack() {
|
||||||
|
this.processing = true;
|
||||||
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.yamlCodeChange();
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.bindTerminal();
|
this.bindTerminal();
|
||||||
} else {
|
} else {
|
||||||
@ -190,20 +360,41 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deployStack() {
|
deployStack() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.bindTerminal();
|
if (!this.jsonConfig.services) {
|
||||||
|
this.$root.toastError("No services found in compose.yaml");
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the stack name if empty, use the first container name
|
||||||
|
if (!this.stack.name) {
|
||||||
|
let serviceName = Object.keys(this.jsonConfig.services)[0];
|
||||||
|
let service = this.jsonConfig.services[serviceName];
|
||||||
|
|
||||||
|
if (service.container_name) {
|
||||||
|
this.stack.name = service.container_name;
|
||||||
|
} else {
|
||||||
|
this.stack.name = serviceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bindTerminal(this.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;
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
this.isEditMode = false;
|
||||||
this.$router.push("/compose/" + this.stack.name);
|
this.$router.push("/compose/" + this.stack.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveStack() {
|
saveStack() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
@ -212,10 +403,48 @@ export default {
|
|||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
this.isEditMode = false;
|
||||||
this.$router.push("/compose/" + this.stack.name);
|
this.$router.push("/compose/" + this.stack.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startStack() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("startStack", this.stack.name, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stopStack() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("stopStack", this.stack.name, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
restartStack() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("restartStack", this.stack.name, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStack() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("updateStack", this.stack.name, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
deleteDialog() {
|
deleteDialog() {
|
||||||
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => {
|
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
@ -233,9 +462,28 @@ export default {
|
|||||||
highlighter(code) {
|
highlighter(code) {
|
||||||
return highlight(code, languages.yaml);
|
return highlight(code, languages.yaml);
|
||||||
},
|
},
|
||||||
|
|
||||||
yamlCodeChange() {
|
yamlCodeChange() {
|
||||||
try {
|
try {
|
||||||
this.jsonConfig = yaml.parse(this.stack.composeYAML) ?? {};
|
let doc = parseDocument(this.stack.composeYAML);
|
||||||
|
if (doc.errors.length > 0) {
|
||||||
|
throw doc.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.yamlDoc = doc;
|
||||||
|
console.log(this.yamlDoc);
|
||||||
|
|
||||||
|
this.jsonConfig = doc.toJS() ?? {};
|
||||||
|
|
||||||
|
if (!this.jsonConfig.version) {
|
||||||
|
this.jsonConfig.version = "3.8";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.jsonConfig.services) {
|
||||||
|
this.jsonConfig.services = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(yamlErrorTimeout);
|
||||||
this.yamlError = "";
|
this.yamlError = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearTimeout(yamlErrorTimeout);
|
clearTimeout(yamlErrorTimeout);
|
||||||
@ -250,6 +498,76 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enableEditMode() {
|
||||||
|
this.isEditMode = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
checkYAML() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addContainer() {
|
||||||
|
this.checkYAML();
|
||||||
|
|
||||||
|
if (this.jsonConfig.services[this.newContainerName]) {
|
||||||
|
this.$root.toastError("Container name already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newContainerName) {
|
||||||
|
this.$root.toastError("Container name cannot be empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jsonConfig.services[this.newContainerName] = {
|
||||||
|
restart: "always",
|
||||||
|
};
|
||||||
|
this.newContainerName = "";
|
||||||
|
let element = this.$refs.containerList.lastElementChild;
|
||||||
|
element.scrollIntoView({
|
||||||
|
block: "start",
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
combineNetworks() {
|
||||||
|
let networks = this.jsonConfig.networks;
|
||||||
|
|
||||||
|
if (!networks) {
|
||||||
|
networks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let serviceName in this.jsonConfig.services) {
|
||||||
|
|
||||||
|
let service = this.jsonConfig.services[serviceName];
|
||||||
|
let serviceNetworks = service.networks;
|
||||||
|
|
||||||
|
if (!networks) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is an array, it should be array of string
|
||||||
|
if (Array.isArray(serviceNetworks)) {
|
||||||
|
for (let n of serviceNetworks) {
|
||||||
|
console.log(n);
|
||||||
|
if (!n) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!networks[n]) {
|
||||||
|
networks[n] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (typeof serviceNetworks === "object") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(networks);
|
||||||
|
|
||||||
|
return networks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -259,7 +577,9 @@ export default {
|
|||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-box.edit-mode {
|
.editor-box {
|
||||||
background-color: #2c2f38 !important;
|
&.edit-mode {
|
||||||
|
background-color: #2c2f38 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,31 +5,35 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Allowed commands: <code>docker</code>, <code>ls</code>, <code>cd</code>
|
Allowed commands:
|
||||||
|
<template v-for="(command, index) in allowedCommandList" :key="command">
|
||||||
|
<code>{{ command }}</code>
|
||||||
|
|
||||||
|
<!-- No comma at the end -->
|
||||||
|
<span v-if="index !== allowedCommandList.length - 1">, </span>
|
||||||
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Terminal ref="terminal" :allow-input="true" class="terminal" :rows="20"></Terminal>
|
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console"></Terminal>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import { allowedCommandList } from "../../../backend/util-common";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allowedCommandList,
|
||||||
|
};
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// 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: {
|
||||||
|
|
||||||
@ -39,6 +43,6 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.terminal {
|
.terminal {
|
||||||
height: calc(100vh - 200px);
|
height: 410px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
48
frontend/src/pages/ContainerTerminal.vue
Normal file
48
frontend/src/pages/ContainerTerminal.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">Console</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Allowed commands:
|
||||||
|
<template v-for="(command, index) in allowedCommandList" :key="command">
|
||||||
|
<code>{{ command }}</code>
|
||||||
|
|
||||||
|
<!-- No comma at the end -->
|
||||||
|
<span v-if="index !== allowedCommandList.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console"></Terminal>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { allowedCommandList } from "../../../backend/util-common";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allowedCommandList,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.terminal {
|
||||||
|
height: 410px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
|
<div v-if="!$root.isMobile" class="col-12 col-md-4 col-xl-3">
|
||||||
<div>
|
<div>
|
||||||
<router-link to="/compose" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("compose") }}</router-link>
|
<router-link to="/compose" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("compose") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<StackList :scrollbar="true" />
|
<StackList :scrollbar="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="container" class="col-12 col-md-7 col-xl-8 mb-3">
|
<div ref="container" class="col-12 col-md-8 col-xl-9 mb-3">
|
||||||
<!-- Add :key to disable vue router re-use the same component -->
|
<!-- Add :key to disable vue router re-use the same component -->
|
||||||
<router-view :key="$route.fullPath" :calculatedHeight="height" />
|
<router-view :key="$route.fullPath" :calculatedHeight="height" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,17 +8,34 @@
|
|||||||
<div class="shadow-box big-padding text-center mb-4">
|
<div class="shadow-box big-padding text-center mb-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>{{ $t("Up") }}</h3>
|
<h3>{{ $t("active") }}</h3>
|
||||||
<span class="num">123</span>
|
<span class="num active">{{ activeNum }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ $t("exited") }}</h3>
|
||||||
|
<span class="num exited">{{ exitedNum }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ $t("inactive") }}</h3>
|
||||||
|
<span class="num inactive">{{ inactiveNum }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mb-3">Docker Run</h2>
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-normal btn" @click="convertDockerRun">Convert to Compose</button>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<router-view ref="child" />
|
<router-view ref="child" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import * as convertDockerRunToCompose from "composerize";
|
||||||
|
import { statusNameShort } from "../../../backend/util-common";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -41,8 +58,22 @@ export default {
|
|||||||
},
|
},
|
||||||
importantHeartBeatListLength: 0,
|
importantHeartBeatListLength: 0,
|
||||||
displayedRecords: [],
|
displayedRecords: [],
|
||||||
|
dockerRunCommand: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
activeNum() {
|
||||||
|
return this.getStatusNum("active");
|
||||||
|
},
|
||||||
|
inactiveNum() {
|
||||||
|
return this.getStatusNum("inactive");
|
||||||
|
},
|
||||||
|
exitedNum() {
|
||||||
|
return this.getStatusNum("exited");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
perPage() {
|
perPage() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -67,6 +98,31 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
getStatusNum(statusName) {
|
||||||
|
let num = 0;
|
||||||
|
|
||||||
|
for (let stackName in this.$root.stackList) {
|
||||||
|
const stack = this.$root.stackList[stackName];
|
||||||
|
if (statusNameShort(stack.status) === statusName) {
|
||||||
|
num += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertDockerRun() {
|
||||||
|
try {
|
||||||
|
if (this.dockerRunCommand.trim() === "docker run") {
|
||||||
|
throw new Error("Please enter a docker run command");
|
||||||
|
}
|
||||||
|
this.$root.composeTemplate = convertDockerRunToCompose(this.dockerRunCommand);
|
||||||
|
this.$router.push("/compose");
|
||||||
|
} catch (e) {
|
||||||
|
this.$root.toastError(e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the displayed records when a new important heartbeat arrives.
|
* Updates the displayed records when a new important heartbeat arrives.
|
||||||
* @param {object} heartbeat - The heartbeat object received.
|
* @param {object} heartbeat - The heartbeat object received.
|
||||||
@ -134,9 +190,17 @@ export default {
|
|||||||
|
|
||||||
.num {
|
.num {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: $primary;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.exited {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
@ -155,4 +219,9 @@ table {
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docker-run {
|
||||||
|
background-color: $dark-bg !important;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
283
frontend/src/pages/Settings.vue
Normal file
283
frontend/src/pages/Settings.vue
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 v-show="show" class="mb-3">
|
||||||
|
{{ $t("Settings") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="shadow-box shadow-box-settings">
|
||||||
|
<div class="row">
|
||||||
|
<div v-if="showSubMenu" class="settings-menu col-lg-3 col-md-5">
|
||||||
|
<router-link
|
||||||
|
v-for="(item, key) in subMenus"
|
||||||
|
:key="key"
|
||||||
|
:to="`/settings/${key}`"
|
||||||
|
>
|
||||||
|
<div class="menu-item">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- Logout Button -->
|
||||||
|
<a v-if="$root.isMobile && $root.loggedIn && $root.socket.token !== 'autoLogin'" class="logout" @click.prevent="$root.logout">
|
||||||
|
<div class="menu-item">
|
||||||
|
<font-awesome-icon icon="sign-out-alt" />
|
||||||
|
{{ $t("Logout") }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="settings-content col-lg-9 col-md-7">
|
||||||
|
<div v-if="currentPage" class="settings-content-header">
|
||||||
|
{{ subMenus[currentPage].title }}
|
||||||
|
</div>
|
||||||
|
<div class="mx-3">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
settings: {},
|
||||||
|
settingsLoaded: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
currentPage() {
|
||||||
|
let pathSplit = useRoute().path.split("/");
|
||||||
|
let pathEnd = pathSplit[pathSplit.length - 1];
|
||||||
|
if (!pathEnd || pathEnd === "settings") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pathEnd;
|
||||||
|
},
|
||||||
|
|
||||||
|
showSubMenu() {
|
||||||
|
if (this.$root.isMobile) {
|
||||||
|
return !this.currentPage;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
subMenus() {
|
||||||
|
return {
|
||||||
|
/*
|
||||||
|
general: {
|
||||||
|
title: this.$t("General"),
|
||||||
|
},*/
|
||||||
|
appearance: {
|
||||||
|
title: this.$t("Appearance"),
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
title: this.$t("Security"),
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
title: this.$t("About"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
"$root.isMobile"() {
|
||||||
|
this.loadGeneralPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadSettings();
|
||||||
|
this.loadGeneralPage();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the general settings page
|
||||||
|
* For desktop only, on mobile do nothing
|
||||||
|
*/
|
||||||
|
loadGeneralPage() {
|
||||||
|
if (!this.currentPage && !this.$root.isMobile) {
|
||||||
|
this.$router.push("/settings/appearance");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Load settings from server */
|
||||||
|
loadSettings() {
|
||||||
|
this.$root.getSocket().emit("getSettings", (res) => {
|
||||||
|
this.settings = res.data;
|
||||||
|
|
||||||
|
if (this.settings.checkUpdate === undefined) {
|
||||||
|
this.settings.checkUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.searchEngineIndex === undefined) {
|
||||||
|
this.settings.searchEngineIndex = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.entryPage === undefined) {
|
||||||
|
this.settings.entryPage = "dashboard";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.nscd === undefined) {
|
||||||
|
this.settings.nscd = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.dnsCache === undefined) {
|
||||||
|
this.settings.dnsCache = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.keepDataPeriodDays === undefined) {
|
||||||
|
this.settings.keepDataPeriodDays = 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.tlsExpiryNotifyDays === undefined) {
|
||||||
|
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.trustProxy === undefined) {
|
||||||
|
this.settings.trustProxy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsLoaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for saving settings
|
||||||
|
* @callback saveSettingsCB
|
||||||
|
* @param {Object} res Result of operation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save Settings
|
||||||
|
* @param {saveSettingsCB} [callback]
|
||||||
|
* @param {string} [currentPassword] Only need for disableAuth to true
|
||||||
|
*/
|
||||||
|
saveSettings(callback, currentPassword) {
|
||||||
|
let valid = this.validateSettings();
|
||||||
|
if (valid.success) {
|
||||||
|
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.loadSettings();
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.toastError(valid.msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure settings are valid
|
||||||
|
* @returns {Object} Contains success state and error msg
|
||||||
|
*/
|
||||||
|
validateSettings() {
|
||||||
|
if (this.settings.keepDataPeriodDays < 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: this.$t("dataRetentionTimeError"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../styles/vars.scss";
|
||||||
|
|
||||||
|
.shadow-box-settings {
|
||||||
|
padding: 20px;
|
||||||
|
min-height: calc(100vh - 155px);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu {
|
||||||
|
a {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 0.5em;
|
||||||
|
padding: 0.7em 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-left-width: 0;
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: $highlight-white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-header-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active .menu-item {
|
||||||
|
background: $highlight-white;
|
||||||
|
border-left: 4px solid $primary;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-header-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
.settings-content-header {
|
||||||
|
width: calc(100% + 20px);
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
border-radius: 0 10px 0 0;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-right: -20px;
|
||||||
|
padding: 12.5px 1em;
|
||||||
|
font-size: 26px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: $dark-header-bg;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile & {
|
||||||
|
padding: 15px 0 0 0;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
color: $danger !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -6,6 +6,15 @@ import Dashboard from "./pages/Dashboard.vue";
|
|||||||
import DashboardHome from "./pages/DashboardHome.vue";
|
import DashboardHome from "./pages/DashboardHome.vue";
|
||||||
import Console from "./pages/Console.vue";
|
import Console from "./pages/Console.vue";
|
||||||
import Compose from "./pages/Compose.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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -30,14 +39,42 @@ const routes = [
|
|||||||
name: "compose",
|
name: "compose",
|
||||||
component: Compose,
|
component: Compose,
|
||||||
props: true,
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/compose/:stackName/terminal/:serviceName/:type",
|
||||||
|
component: ContainerTerminal,
|
||||||
|
name: "containerTerminal",
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/console",
|
path: "/console",
|
||||||
component: Console,
|
component: Console,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/settings",
|
||||||
|
component: Settings,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "general",
|
||||||
|
component: General,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "appearance",
|
||||||
|
component: Appearance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "security",
|
||||||
|
component: Security,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "about",
|
||||||
|
component: About,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -35,6 +35,10 @@ textarea.form-control {
|
|||||||
color: $maintenance !important;
|
color: $maintenance !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: $dark-font-color3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.incident a,
|
.incident a,
|
||||||
.bg-maintenance a {
|
.bg-maintenance a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@ -45,7 +49,7 @@ textarea.form-control {
|
|||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: $dark-bg;
|
background-color: $dark-bg2;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
border-color: $dark-border-color;
|
border-color: $dark-border-color;
|
||||||
}
|
}
|
||||||
@ -320,6 +324,7 @@ optgroup {
|
|||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
color: $dark-font-color2;
|
color: $dark-font-color2;
|
||||||
|
background: $primary-gradient;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@ -486,21 +491,19 @@ optgroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 52px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 13px 15px 10px 15px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $highlight-white;
|
background-color: $highlight-white;
|
||||||
}
|
}
|
||||||
@ -508,9 +511,10 @@ optgroup {
|
|||||||
&.active {
|
&.active {
|
||||||
background-color: #cdf8f4;
|
background-color: #cdf8f4;
|
||||||
}
|
}
|
||||||
.tags {
|
|
||||||
// Removes margin to line up tags list with uptime percentage
|
.title {
|
||||||
margin-left: -0.25rem;
|
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
|
// Localization
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
@ -212,3 +212,4 @@ export function getToastErrorTimeout() {
|
|||||||
|
|
||||||
return errorTimeout;
|
return errorTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
"composerize": "^1.4.1",
|
||||||
"croner": "^7.0.4",
|
"croner": "^7.0.4",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
|
"@types/bootstrap": "^5.2.8",
|
||||||
"@types/command-exists": "~1.2.2",
|
"@types/command-exists": "~1.2.2",
|
||||||
"@types/express": "~4.17.20",
|
"@types/express": "~4.17.20",
|
||||||
"@types/jsonwebtoken": "~9.0.4",
|
"@types/jsonwebtoken": "~9.0.4",
|
||||||
@ -70,6 +72,7 @@
|
|||||||
"vue-eslint-parser": "^9.3.2",
|
"vue-eslint-parser": "^9.3.2",
|
||||||
"vue-i18n": "~9.5.0",
|
"vue-i18n": "~9.5.0",
|
||||||
"vue-prism-editor": "~2.0.0-alpha.2",
|
"vue-prism-editor": "~2.0.0-alpha.2",
|
||||||
|
"vue-qrcode": "^2.2.0",
|
||||||
"vue-router": "~4.2.5",
|
"vue-router": "~4.2.5",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"xterm": "~5.3.0",
|
"xterm": "~5.3.0",
|
||||||
|
211
pnpm-lock.yaml
generated
211
pnpm-lock.yaml
generated
@ -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
|
||||||
|
composerize:
|
||||||
|
specifier: ^1.4.1
|
||||||
|
version: 1.4.1
|
||||||
croner:
|
croner:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.0.4
|
version: 7.0.4
|
||||||
@ -93,6 +96,9 @@ devDependencies:
|
|||||||
'@fortawesome/vue-fontawesome':
|
'@fortawesome/vue-fontawesome':
|
||||||
specifier: 3.0.3
|
specifier: 3.0.3
|
||||||
version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.6)
|
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':
|
'@types/command-exists':
|
||||||
specifier: ~1.2.2
|
specifier: ~1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
@ -165,6 +171,9 @@ devDependencies:
|
|||||||
vue-prism-editor:
|
vue-prism-editor:
|
||||||
specifier: ~2.0.0-alpha.2
|
specifier: ~2.0.0-alpha.2
|
||||||
version: 2.0.0-alpha.2(vue@3.3.6)
|
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:
|
vue-router:
|
||||||
specifier: ~4.2.5
|
specifier: ~4.2.5
|
||||||
version: 4.2.5(vue@3.3.6)
|
version: 4.2.5(vue@3.3.6)
|
||||||
@ -809,6 +818,12 @@ packages:
|
|||||||
'@types/node': 20.8.7
|
'@types/node': 20.8.7
|
||||||
dev: true
|
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:
|
/@types/command-exists@1.2.2:
|
||||||
resolution: {integrity: sha512-1qKPTkjLmghE5C7UUHXGcLaG8MNftchOcLAIryUXNKahRO5beS+iJ9rIL8XD4+B8K2phjYUsPQDox1FRX4KMTQ==}
|
resolution: {integrity: sha512-1qKPTkjLmghE5C7UUHXGcLaG8MNftchOcLAIryUXNKahRO5beS+iJ9rIL8XD4+B8K2phjYUsPQDox1FRX4KMTQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1330,7 +1345,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js: 1.0.3
|
sprintf-js: 1.0.3
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
|
||||||
|
|
||||||
/argparse@2.0.1:
|
/argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
@ -1578,6 +1592,10 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/camelcase@5.3.1:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
/chalk@2.4.2:
|
/chalk@2.4.2:
|
||||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -1653,6 +1671,14 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
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:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1730,6 +1756,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==}
|
resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==}
|
||||||
dev: false
|
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:
|
/concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@ -1772,6 +1809,12 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
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:
|
/cors@2.8.5:
|
||||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@ -1875,6 +1918,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
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:
|
/decompress-response@6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1891,6 +1938,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
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:
|
/define-data-property@1.1.1:
|
||||||
resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
|
resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1935,6 +1987,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dijkstrajs@1.0.3:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/dir-glob@3.0.1:
|
/dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1974,12 +2030,15 @@ packages:
|
|||||||
/emoji-regex@8.0.0:
|
/emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/emoji-regex@9.2.2:
|
/emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/encode-utf8@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/encodeurl@1.0.2:
|
/encodeurl@1.0.2:
|
||||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -2417,6 +2476,14 @@ packages:
|
|||||||
array-back: 3.1.0
|
array-back: 3.1.0
|
||||||
dev: false
|
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:
|
/find-up@5.0.0:
|
||||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2552,6 +2619,11 @@ packages:
|
|||||||
is-property: 1.0.2
|
is-property: 1.0.2
|
||||||
dev: false
|
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:
|
/get-intrinsic@1.2.2:
|
||||||
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
|
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2861,6 +2933,12 @@ packages:
|
|||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/invariant@2.2.4:
|
||||||
|
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ip@1.1.8:
|
/ip@1.1.8:
|
||||||
resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
|
resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@ -2904,7 +2982,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/is-glob@4.0.3:
|
/is-glob@4.0.3:
|
||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
@ -2955,6 +3032,10 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/js-tokens@4.0.0:
|
||||||
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/js-yaml@4.1.0:
|
/js-yaml@4.1.0:
|
||||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -3170,6 +3251,13 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dev: true
|
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:
|
/locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3227,6 +3315,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
||||||
dev: false
|
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:
|
/lru-cache@10.0.1:
|
||||||
resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==}
|
resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==}
|
||||||
engines: {node: 14 || >=16.14}
|
engines: {node: 14 || >=16.14}
|
||||||
@ -3706,6 +3801,13 @@ packages:
|
|||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
dev: true
|
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:
|
/p-limit@3.1.0:
|
||||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3713,6 +3815,13 @@ packages:
|
|||||||
yocto-queue: 0.1.0
|
yocto-queue: 0.1.0
|
||||||
dev: true
|
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:
|
/p-locate@5.0.0:
|
||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3729,6 +3838,11 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
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:
|
/pac-proxy-agent@7.0.1:
|
||||||
resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==}
|
resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@ -3949,6 +4063,11 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/postcss-selector-parser@6.0.13:
|
/postcss-selector-parser@6.0.13:
|
||||||
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
|
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -4074,6 +4193,17 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
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:
|
/qs@6.11.0:
|
||||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@ -4166,6 +4296,11 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
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:
|
/require-in-the-middle@5.2.0:
|
||||||
resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==}
|
resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -4179,6 +4314,10 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/require-main-filename@2.0.0:
|
||||||
|
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -4321,7 +4460,6 @@ packages:
|
|||||||
|
|
||||||
/set-blocking@2.0.0:
|
/set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/set-function-length@1.1.1:
|
/set-function-length@1.1.1:
|
||||||
resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
|
resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
|
||||||
@ -4516,7 +4654,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
|
||||||
|
|
||||||
/sprintf-js@1.1.2:
|
/sprintf-js@1.1.2:
|
||||||
resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==}
|
resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==}
|
||||||
@ -4554,7 +4691,6 @@ packages:
|
|||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/string-width@5.1.2:
|
/string-width@5.1.2:
|
||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
@ -4729,8 +4865,6 @@ packages:
|
|||||||
/tslib@2.6.2:
|
/tslib@2.6.2:
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/tsx@3.14.0:
|
/tsx@3.14.0:
|
||||||
resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==}
|
resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==}
|
||||||
@ -5045,6 +5179,17 @@ packages:
|
|||||||
vue: 3.3.6(typescript@5.2.2)
|
vue: 3.3.6(typescript@5.2.2)
|
||||||
dev: true
|
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):
|
/vue-router@4.2.5(vue@3.3.6):
|
||||||
resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
|
resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5098,6 +5243,10 @@ packages:
|
|||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/which-module@2.0.1:
|
||||||
|
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/which@2.0.2:
|
/which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -5119,6 +5268,15 @@ packages:
|
|||||||
typical: 5.2.0
|
typical: 5.2.0
|
||||||
dev: false
|
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:
|
/wrap-ansi@7.0.0:
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -5206,6 +5364,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/y18n@4.0.3:
|
||||||
|
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yallist@4.0.0:
|
/yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
@ -5221,7 +5383,38 @@ packages:
|
|||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
dev: false
|
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:
|
/yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
|
Loading…
Reference in New Issue
Block a user