mirror of
https://github.com/louislam/dockge.git
synced 2024-11-28 11:14:31 +01:00
wip
This commit is contained in:
parent
5f70fa6baf
commit
7d1da2ad99
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@ -0,0 +1,17 @@
|
||||
# Should be identical to .gitignore
|
||||
.env
|
||||
node_modules
|
||||
dist
|
||||
frontend-dist
|
||||
.idea
|
||||
data
|
||||
tmp
|
||||
/private
|
||||
|
||||
# Docker extra
|
||||
docker
|
||||
frontend
|
||||
.editorconfig
|
||||
.eslintrc.cjs
|
||||
.gitignore
|
||||
README.md
|
@ -92,5 +92,6 @@ module.exports = {
|
||||
"one-var": [ "error", "never" ],
|
||||
"max-statements-per-line": [ "error", { "max": 1 }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"prefer-const" : "off",
|
||||
},
|
||||
};
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,8 +1,10 @@
|
||||
# dotenv environment variable files
|
||||
# Should update .dockerignore as well
|
||||
.env
|
||||
node_modules
|
||||
dist
|
||||
frontend-dist
|
||||
.idea
|
||||
data
|
||||
tmp
|
||||
/private
|
||||
|
||||
|
34
README.md
34
README.md
@ -1,21 +1,33 @@
|
||||
<div align="center" width="100%">
|
||||
<img src="./frontend/public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
# Dockge
|
||||
|
||||
## Features
|
||||
A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.
|
||||
|
||||
- Easy-to-use
|
||||
- Fancy UI
|
||||
- Focus on `docker-compose` stack management
|
||||
## ⭐ Features
|
||||
|
||||
- Focus on `docker-compose.yml` stack management
|
||||
- Interactive editor for `docker-compose.yml` files
|
||||
- Easy to expose your service to the internet with https
|
||||
- Interactive web terminal for containers and any docker commands
|
||||
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
|
||||
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too
|
||||
|
||||
## Installation
|
||||
|
||||
## Motivations
|
||||
|
||||
- Want to build my dream web-based container manager
|
||||
- Want to try next gen runtime like Deno or Bun, but I chose Deno because at this moment, Deno is more stable and Jetbrains IDE support is better.
|
||||
- Full TypeScript and ES module
|
||||
- Try DaisyUI + TailwindCSS
|
||||
- Try ES Module and TypeScript in 2023
|
||||
- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, it keeps spinning the loading icon for a few minutes, and I don't know what's going on.
|
||||
|
||||
## Dockge?
|
||||
If you love this project, please consider giving this project a ⭐.
|
||||
|
||||
Naming idea is coming from Twitch emotes. There are many emotes sound like this such as `bedge` and `sadge`.
|
||||
## More Ideas?
|
||||
|
||||
- Container file manager
|
||||
- App store for yaml templates
|
||||
- Container stats
|
||||
- Get app icons
|
||||
- Switch Docker context
|
||||
- Zero-config private docker registry
|
||||
|
@ -20,27 +20,20 @@ import { R } from "redbean-node";
|
||||
import { genSecret, isDev } from "./util-common";
|
||||
import { generatePasswordHash } from "./password-hash";
|
||||
import { Bean } from "redbean-node/dist/bean";
|
||||
import { DockgeSocket } from "./util-server";
|
||||
import { Arguments, Config, DockgeSocket } from "./util-server";
|
||||
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
|
||||
import { Terminal } from "./terminal";
|
||||
|
||||
export interface Arguments {
|
||||
sslKey? : string;
|
||||
sslCert? : string;
|
||||
sslKeyPassphrase? : string;
|
||||
port? : number;
|
||||
hostname? : string;
|
||||
dataDir? : string;
|
||||
}
|
||||
import expressStaticGzip from "express-static-gzip";
|
||||
import path from "path";
|
||||
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
|
||||
import { Stack } from "./stack";
|
||||
|
||||
export class DockgeServer {
|
||||
app : Express;
|
||||
httpServer : http.Server;
|
||||
packageJSON : PackageJson;
|
||||
io : socketIO.Server;
|
||||
config : Arguments;
|
||||
indexHTML : string;
|
||||
terminal : Terminal;
|
||||
config : Config;
|
||||
indexHTML : string = "";
|
||||
|
||||
/**
|
||||
* List of express routers
|
||||
@ -55,6 +48,7 @@ export class DockgeServer {
|
||||
socketHandlerList : SocketHandler[] = [
|
||||
new MainSocketHandler(),
|
||||
new DockerSocketHandler(),
|
||||
new TerminalSocketHandler(),
|
||||
];
|
||||
|
||||
/**
|
||||
@ -64,10 +58,20 @@ export class DockgeServer {
|
||||
|
||||
jwtSecret? : string;
|
||||
|
||||
stacksDir : string = "";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
// Catch unexpected errors here
|
||||
let unexpectedErrorHandler = (error : unknown) => {
|
||||
console.trace(error);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
};
|
||||
process.addListener("unhandledRejection", unexpectedErrorHandler);
|
||||
process.addListener("uncaughtException", unexpectedErrorHandler);
|
||||
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
@ -75,8 +79,8 @@ export class DockgeServer {
|
||||
// Log NODE ENV
|
||||
log.info("server", "NODE_ENV: " + process.env.NODE_ENV);
|
||||
|
||||
// Load arguments
|
||||
const args = this.config = parse<Arguments>({
|
||||
// Define all possible arguments
|
||||
let args = parse<Arguments>({
|
||||
sslKey: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@ -103,21 +107,101 @@ export class DockgeServer {
|
||||
}
|
||||
});
|
||||
|
||||
// Load from environment variables or default values if args are not set
|
||||
args.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
|
||||
args.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
|
||||
args.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
|
||||
args.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
|
||||
args.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
|
||||
args.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
|
||||
this.config = args as Config;
|
||||
|
||||
log.debug("server", args);
|
||||
// Load from environment variables or default values if args are not set
|
||||
this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
|
||||
this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
|
||||
this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
|
||||
this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
|
||||
this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
|
||||
this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
|
||||
|
||||
log.debug("server", this.config);
|
||||
|
||||
this.packageJSON = packageJSON as PackageJson;
|
||||
|
||||
try {
|
||||
this.indexHTML = fs.readFileSync("./frontend-dist/index.html").toString();
|
||||
} catch (e) {
|
||||
// "dist/index.html" is not necessary for development
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
log.error("server", "Error: Cannot find 'frontend-dist/index.html', did you install correctly?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Create all the necessary directories
|
||||
this.initDataDir();
|
||||
|
||||
this.terminal = new Terminal(this);
|
||||
// Create express
|
||||
this.app = express();
|
||||
|
||||
// Create HTTP server
|
||||
if (this.config.sslKey && this.config.sslCert) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(this.config.sslKey),
|
||||
cert: fs.readFileSync(this.config.sslCert),
|
||||
passphrase: this.config.sslKeyPassphrase,
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
this.httpServer = http.createServer(this.app);
|
||||
}
|
||||
|
||||
// Binding Routers
|
||||
for (const router of this.routerList) {
|
||||
this.app.use(router.create(this.app, this));
|
||||
}
|
||||
|
||||
// Static files
|
||||
this.app.use("/", expressStaticGzip("frontend-dist", {
|
||||
enableBrotli: true,
|
||||
}));
|
||||
|
||||
// Universal Route Handler, must be at the end of all express routes.
|
||||
this.app.get("*", async (_request, response) => {
|
||||
response.send(this.indexHTML);
|
||||
});
|
||||
|
||||
// Allow all CORS origins in development
|
||||
let cors = undefined;
|
||||
if (isDev) {
|
||||
cors = {
|
||||
origin: "*",
|
||||
};
|
||||
}
|
||||
|
||||
// Create Socket.io
|
||||
this.io = new socketIO.Server(this.httpServer, {
|
||||
cors,
|
||||
});
|
||||
|
||||
this.io.on("connection", (socket: Socket) => {
|
||||
log.info("server", "Socket connected!");
|
||||
|
||||
this.sendInfo(socket, true);
|
||||
|
||||
if (this.needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
socket.emit("setup");
|
||||
}
|
||||
|
||||
// Create socket handlers
|
||||
for (const socketHandler of this.socketHandlerList) {
|
||||
socketHandler.create(socket as DockgeSocket, this);
|
||||
}
|
||||
});
|
||||
|
||||
this.io.on("disconnect", () => {
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
prepareServer() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,64 +241,6 @@ export class DockgeServer {
|
||||
this.needSetup = true;
|
||||
}
|
||||
|
||||
// Create express
|
||||
this.app = express();
|
||||
|
||||
if (this.config.sslKey && this.config.sslCert) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(this.config.sslKey),
|
||||
cert: fs.readFileSync(this.config.sslCert),
|
||||
passphrase: this.config.sslKeyPassphrase,
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
this.httpServer = http.createServer(this.app);
|
||||
}
|
||||
|
||||
try {
|
||||
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
} catch (e) {
|
||||
// "dist/index.html" is not necessary for development
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const router of this.routerList) {
|
||||
this.app.use(router.create(this.app, this));
|
||||
}
|
||||
|
||||
let cors = undefined;
|
||||
|
||||
if (isDev) {
|
||||
cors = {
|
||||
origin: "*",
|
||||
};
|
||||
}
|
||||
|
||||
// Create Socket.io
|
||||
this.io = new socketIO.Server(this.httpServer, {
|
||||
cors,
|
||||
});
|
||||
|
||||
this.io.on("connection", (socket: Socket) => {
|
||||
log.info("server", "Socket connected!");
|
||||
|
||||
this.sendInfo(socket, true);
|
||||
|
||||
if (this.needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
socket.emit("setup");
|
||||
}
|
||||
|
||||
// Create socket handlers
|
||||
for (const socketHandler of this.socketHandlerList) {
|
||||
socketHandler.create(socket as DockgeSocket, this);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen
|
||||
this.httpServer.listen(5001, this.config.hostname, () => {
|
||||
if (this.config.hostname) {
|
||||
@ -349,14 +375,21 @@ export class DockgeServer {
|
||||
* Initialize the data directory
|
||||
*/
|
||||
initDataDir() {
|
||||
if (! fs.existsSync(this.config.dataDir)) {
|
||||
fs.mkdirSync(this.config.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check if a directory
|
||||
if (!fs.lstatSync(this.config.dataDir).isDirectory()) {
|
||||
throw new Error(`Fatal error: ${this.config.dataDir} is not a directory`);
|
||||
}
|
||||
|
||||
if (! fs.existsSync(this.config.dataDir)) {
|
||||
fs.mkdirSync(this.config.dataDir, { recursive: true });
|
||||
// Create data/stacks directory
|
||||
this.stacksDir = path.join(this.config.dataDir, "stacks");
|
||||
if (!fs.existsSync(this.stacksDir)) {
|
||||
fs.mkdirSync(this.stacksDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info("server", `Data Dir: ${this.config.dataDir}`);
|
||||
}
|
||||
|
||||
@ -378,4 +411,19 @@ export class DockgeServer {
|
||||
await R.store(jwtSecretBean);
|
||||
return jwtSecretBean;
|
||||
}
|
||||
|
||||
sendStackList(socket : DockgeSocket) {
|
||||
let room = socket.userID.toString();
|
||||
let stackList = Stack.getStackList(this);
|
||||
let list = {};
|
||||
|
||||
for (let stack of stackList) {
|
||||
list[stack.name] = stack.toSimpleJSON();
|
||||
}
|
||||
|
||||
this.io.to(room).emit("stackList", {
|
||||
ok: true,
|
||||
stackList: list,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export class MainRouter extends Router {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
|
||||
res.send(server.indexHTML);
|
||||
});
|
||||
|
||||
return router;
|
||||
|
@ -1,61 +1,84 @@
|
||||
import { SocketHandler } from "../socket-handler.js";
|
||||
import { DockgeServer } from "../dockge-server";
|
||||
import { checkLogin, DockgeSocket } from "../util-server";
|
||||
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||
import { log } from "../log";
|
||||
|
||||
const allowedCommandList : string[] = [
|
||||
"docker",
|
||||
];
|
||||
import yaml from "yaml";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import {
|
||||
allowedCommandList,
|
||||
allowedRawKeys,
|
||||
getComposeTerminalName,
|
||||
isDev,
|
||||
PROGRESS_TERMINAL_ROWS
|
||||
} from "../util-common";
|
||||
import { Terminal } from "../terminal";
|
||||
import { Stack } from "../stack";
|
||||
|
||||
export class DockerSocketHandler extends SocketHandler {
|
||||
create(socket : DockgeSocket, server : DockgeServer) {
|
||||
|
||||
socket.on("composeUp", async (compose, callback) => {
|
||||
|
||||
});
|
||||
|
||||
socket.on("terminalInput", async (cmd : unknown, errorCallback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(cmd) !== "string") {
|
||||
throw new Error("Command must be a string.");
|
||||
}
|
||||
|
||||
// Check if the command is allowed
|
||||
const cmdParts = cmd.split(" ");
|
||||
const executable = cmdParts[0].trim();
|
||||
log.debug("console", "Executable: " + executable);
|
||||
log.debug("console", "Executable length: " + executable.length);
|
||||
|
||||
if (!allowedCommandList.includes(executable)) {
|
||||
throw new Error("Command not allowed.");
|
||||
}
|
||||
|
||||
server.terminal.write(cmd);
|
||||
} catch (e) {
|
||||
errorCallback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setup
|
||||
socket.on("getTerminalBuffer", async (callback) => {
|
||||
socket.on("deployStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
const stack = this.saveStack(socket, server, name, composeYAML, isAdd);
|
||||
await stack.deploy(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
buffer: server.terminal.getBuffer(),
|
||||
});
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("saveStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
this.saveStack(socket, server, name, composeYAML, isAdd);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
ok: true,
|
||||
"msg": "Saved"
|
||||
});
|
||||
server.sendStackList(socket);
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getStack", (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);
|
||||
callback({
|
||||
ok: true,
|
||||
stack: stack.toJSON(),
|
||||
});
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {
|
||||
// Check types
|
||||
if (typeof(name) !== "string") {
|
||||
throw new ValidationError("Name must be a string");
|
||||
}
|
||||
if (typeof(composeYAML) !== "string") {
|
||||
throw new ValidationError("Compose YAML must be a string");
|
||||
}
|
||||
if (typeof(isAdd) !== "boolean") {
|
||||
throw new ValidationError("isAdd must be a boolean");
|
||||
}
|
||||
|
||||
const stack = new Stack(server, name, composeYAML);
|
||||
stack.save(isAdd);
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ export class MainSocketHandler extends SocketHandler {
|
||||
}
|
||||
|
||||
log.debug("auth", "afterLogin");
|
||||
await this.afterLogin(socket, user);
|
||||
await this.afterLogin(server, socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||
@ -128,7 +128,7 @@ export class MainSocketHandler extends SocketHandler {
|
||||
|
||||
if (user) {
|
||||
if (user.twofa_status === 0) {
|
||||
this.afterLogin(socket, user);
|
||||
this.afterLogin(server, socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
@ -151,7 +151,7 @@ export class MainSocketHandler extends SocketHandler {
|
||||
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
|
||||
|
||||
if (user.twofa_last_token !== data.token && verify) {
|
||||
this.afterLogin(socket, user);
|
||||
this.afterLogin(server, socket, user);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
|
||||
data.token,
|
||||
@ -189,10 +189,15 @@ export class MainSocketHandler extends SocketHandler {
|
||||
});
|
||||
}
|
||||
|
||||
async afterLogin(socket : DockgeSocket, user : User) {
|
||||
async afterLogin(server: DockgeServer, socket : DockgeSocket, user : User) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id + "");
|
||||
socket.join("terminal");
|
||||
socket.join(user.id.toString());
|
||||
|
||||
try {
|
||||
server.sendStackList(socket);
|
||||
} catch (e) {
|
||||
log.error("server", e);
|
||||
}
|
||||
}
|
||||
|
||||
async login(username : string, password : string) {
|
||||
|
111
backend/socket-handlers/terminal-socket-handler.ts
Normal file
111
backend/socket-handlers/terminal-socket-handler.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { SocketHandler } from "../socket-handler.js";
|
||||
import { DockgeServer } from "../dockge-server";
|
||||
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||
import { log } from "../log";
|
||||
import yaml from "yaml";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { allowedCommandList, allowedRawKeys, isDev } from "../util-common";
|
||||
import { Terminal } from "../terminal";
|
||||
|
||||
export class TerminalSocketHandler extends SocketHandler {
|
||||
create(socket : DockgeSocket, server : DockgeServer) {
|
||||
|
||||
socket.on("terminalInputRaw", async (key : unknown) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(key) !== "string") {
|
||||
throw new Error("Key must be a string.");
|
||||
}
|
||||
|
||||
if (allowedRawKeys.includes(key)) {
|
||||
server.terminal.write(key);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback : unknown) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(cmd) !== "string") {
|
||||
throw new Error("Command must be a string.");
|
||||
}
|
||||
|
||||
// Check if the command is allowed
|
||||
const cmdParts = cmd.split(" ");
|
||||
const executable = cmdParts[0].trim();
|
||||
log.debug("console", "Executable: " + executable);
|
||||
log.debug("console", "Executable length: " + executable.length);
|
||||
|
||||
if (!allowedCommandList.includes(executable)) {
|
||||
throw new Error("Command not allowed.");
|
||||
}
|
||||
|
||||
server.terminal.write(cmd);
|
||||
} catch (e) {
|
||||
if (typeof(errorCallback) === "function") {
|
||||
errorCallback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create Terminal
|
||||
socket.on("terminalCreate", async (terminalName : unknown, callback : unknown) => {
|
||||
|
||||
});
|
||||
|
||||
// Join Terminal
|
||||
socket.on("terminalJoin", async (terminalName : unknown, callback : unknown) => {
|
||||
if (typeof(callback) !== "function") {
|
||||
log.debug("console", "Callback is not a function.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (typeof(terminalName) !== "string") {
|
||||
throw new ValidationError("Terminal name must be a string.");
|
||||
}
|
||||
|
||||
let buffer : string = Terminal.getTerminal(terminalName)?.getBuffer() ?? "";
|
||||
|
||||
if (!buffer) {
|
||||
log.debug("console", "No buffer found.");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
buffer,
|
||||
});
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// Close Terminal
|
||||
socket.on("terminalClose", async (terminalName : unknown, callback : unknown) => {
|
||||
|
||||
});
|
||||
|
||||
// Resize Terminal
|
||||
socket.on("terminalResize", async (rows : unknown) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (typeof(rows) !== "number") {
|
||||
throw new Error("Rows must be a number.");
|
||||
}
|
||||
log.debug("console", "Resize terminal to " + rows + " rows.");
|
||||
server.terminal.resize(rows);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
158
backend/stack.ts
Normal file
158
backend/stack.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { DockgeServer } from "./dockge-server";
|
||||
import fs from "fs";
|
||||
import { log } from "./log";
|
||||
import yaml from "yaml";
|
||||
import { DockgeSocket, ValidationError } from "./util-server";
|
||||
import path from "path";
|
||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "./util-common";
|
||||
import { Terminal } from "./terminal";
|
||||
|
||||
export class Stack {
|
||||
|
||||
name: string;
|
||||
protected _composeYAML?: string;
|
||||
protected server: DockgeServer;
|
||||
|
||||
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
||||
this.name = name;
|
||||
this.server = server;
|
||||
this._composeYAML = composeYAML;
|
||||
}
|
||||
|
||||
toJSON() : object {
|
||||
let obj = this.toSimpleJSON();
|
||||
return {
|
||||
...obj,
|
||||
composeYAML: this.composeYAML,
|
||||
};
|
||||
}
|
||||
|
||||
toSimpleJSON() : object {
|
||||
return {
|
||||
name: this.name,
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
|
||||
validate() {
|
||||
// Check name, allows [a-z][A-Z][0-9] _ - only
|
||||
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
|
||||
}
|
||||
|
||||
// Check YAML format
|
||||
yaml.parse(this.composeYAML);
|
||||
}
|
||||
|
||||
get composeYAML() : string {
|
||||
if (this._composeYAML === undefined) {
|
||||
try {
|
||||
this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
|
||||
} catch (e) {
|
||||
this._composeYAML = "";
|
||||
}
|
||||
}
|
||||
return this._composeYAML;
|
||||
}
|
||||
|
||||
get path() : string {
|
||||
return path.join(this.server.stacksDir, this.name);
|
||||
}
|
||||
|
||||
get fullPath() : string {
|
||||
let dir = this.path;
|
||||
|
||||
// Compose up via node-pty
|
||||
let fullPathDir;
|
||||
|
||||
// if dir is relative, make it absolute
|
||||
if (!path.isAbsolute(dir)) {
|
||||
fullPathDir = path.join(process.cwd(), dir);
|
||||
} else {
|
||||
fullPathDir = dir;
|
||||
}
|
||||
return fullPathDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the stack to the disk
|
||||
* @param isAdd
|
||||
*/
|
||||
save(isAdd : boolean) {
|
||||
this.validate();
|
||||
|
||||
let dir = this.path;
|
||||
|
||||
// Check if the name is used if isAdd
|
||||
if (isAdd) {
|
||||
if (fs.existsSync(dir)) {
|
||||
throw new ValidationError("Stack name already exists");
|
||||
}
|
||||
|
||||
// Create the stack folder
|
||||
fs.mkdirSync(dir);
|
||||
} else {
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new ValidationError("Stack not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Write or overwrite the compose.yaml
|
||||
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
||||
}
|
||||
|
||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
||||
const terminalName = getComposeTerminalName(this.name);
|
||||
log.debug("deployStack", "Terminal name: " + terminalName);
|
||||
|
||||
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "up", "-d" ], this.path);
|
||||
log.debug("deployStack", "Terminal created");
|
||||
|
||||
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
||||
|
||||
if (socket) {
|
||||
terminal.join(socket);
|
||||
log.debug("deployStack", "Terminal joined");
|
||||
} else {
|
||||
log.debug("deployStack", "No socket, not joining");
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
static getStackList(server : DockgeServer) : Stack[] {
|
||||
let stacksDir = server.stacksDir;
|
||||
let stackList : Stack[] = [];
|
||||
|
||||
// Scan the stacks directory, and get the stack list
|
||||
let filenameList = fs.readdirSync(stacksDir);
|
||||
|
||||
log.debug("stack", filenameList);
|
||||
|
||||
for (let filename of filenameList) {
|
||||
let relativePath = path.join(stacksDir, filename);
|
||||
if (fs.statSync(relativePath).isDirectory()) {
|
||||
let stack = new Stack(server, filename);
|
||||
stackList.push(stack);
|
||||
}
|
||||
}
|
||||
return stackList;
|
||||
}
|
||||
|
||||
static getStack(server: DockgeServer, stackName: string) : Stack {
|
||||
let dir = path.join(server.stacksDir, stackName);
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new ValidationError("Stack not found");
|
||||
}
|
||||
return new Stack(server, stackName);
|
||||
}
|
||||
}
|
@ -1,43 +1,151 @@
|
||||
import { DockgeServer } from "./dockge-server";
|
||||
import * as os from "node:os";
|
||||
import * as pty from "node-pty";
|
||||
import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
|
||||
import { LimitQueue } from "./utils/limit-queue";
|
||||
import { DockgeSocket } from "./util-server";
|
||||
import { getCryptoRandomInt, TERMINAL_COLS, TERMINAL_ROWS } from "./util-common";
|
||||
import { sync as commandExistsSync } from "command-exists";
|
||||
import { log } from "./log";
|
||||
|
||||
const shell = os.platform() === "win32" ? "pwsh.exe" : "bash";
|
||||
|
||||
/**
|
||||
* Terminal for running commands, no user interaction
|
||||
*/
|
||||
export class Terminal {
|
||||
|
||||
ptyProcess;
|
||||
private server : DockgeServer;
|
||||
private buffer : LimitQueue<string> = new LimitQueue(100);
|
||||
protected static terminalMap : Map<string, Terminal> = new Map();
|
||||
|
||||
constructor(server : DockgeServer) {
|
||||
protected _ptyProcess? : pty.IPty;
|
||||
protected server : DockgeServer;
|
||||
protected buffer : LimitQueue<string> = new LimitQueue(100);
|
||||
protected _name : string;
|
||||
|
||||
protected file : string;
|
||||
protected args : string | string[];
|
||||
protected cwd : string;
|
||||
protected callback? : (exitCode : number) => void;
|
||||
|
||||
protected _rows : number = TERMINAL_ROWS;
|
||||
|
||||
constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
|
||||
this.server = server;
|
||||
this._name = name;
|
||||
//this._name = "terminal-" + Date.now() + "-" + getCryptoRandomInt(0, 1000000);
|
||||
this.file = file;
|
||||
this.args = args;
|
||||
this.cwd = cwd;
|
||||
|
||||
this.ptyProcess = pty.spawn(shell, [], {
|
||||
name: "dockge-terminal",
|
||||
cwd: "./tmp",
|
||||
});
|
||||
|
||||
// this.ptyProcess.write("npm remove lodash\r");
|
||||
//this.ptyProcess.write("npm install lodash\r");
|
||||
|
||||
this.ptyProcess.onData((data) => {
|
||||
this.buffer.push(data);
|
||||
this.server.io.to("terminal").emit("commandOutput", data);
|
||||
});
|
||||
|
||||
Terminal.terminalMap.set(this.name, this);
|
||||
}
|
||||
|
||||
write(input : string) {
|
||||
this.ptyProcess.write(input);
|
||||
get rows() {
|
||||
return this._rows;
|
||||
}
|
||||
|
||||
set rows(rows : number) {
|
||||
this._rows = rows;
|
||||
this.ptyProcess?.resize(TERMINAL_COLS, rows);
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._ptyProcess = pty.spawn(this.file, this.args, {
|
||||
name: this.name,
|
||||
cwd: this.cwd,
|
||||
cols: TERMINAL_COLS,
|
||||
rows: this.rows,
|
||||
});
|
||||
|
||||
// On Data
|
||||
this._ptyProcess.onData((data) => {
|
||||
this.buffer.push(data);
|
||||
if (this.server.io) {
|
||||
this.server.io.to(this.name).emit("terminalWrite", this.name, data);
|
||||
}
|
||||
});
|
||||
|
||||
// On Exit
|
||||
this._ptyProcess.onExit((res) => {
|
||||
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
|
||||
|
||||
// Remove room
|
||||
this.server.io.in(this.name).socketsLeave(this.name);
|
||||
|
||||
if (this.callback) {
|
||||
this.callback(res.exitCode);
|
||||
}
|
||||
|
||||
Terminal.terminalMap.delete(this.name);
|
||||
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
|
||||
});
|
||||
}
|
||||
|
||||
public onExit(callback : (exitCode : number) => void) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public join(socket : DockgeSocket) {
|
||||
socket.join(this.name);
|
||||
}
|
||||
|
||||
public leave(socket : DockgeSocket) {
|
||||
socket.leave(this.name);
|
||||
}
|
||||
|
||||
public get ptyProcess() {
|
||||
return this._ptyProcess;
|
||||
}
|
||||
|
||||
public get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the terminal output string for re-connecting
|
||||
* Get the terminal output string
|
||||
*/
|
||||
getBuffer() : string {
|
||||
if (this.buffer.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return this.buffer.join("");
|
||||
}
|
||||
|
||||
close() {
|
||||
this._ptyProcess?.kill();
|
||||
}
|
||||
|
||||
public static getTerminal(name : string) : Terminal | undefined {
|
||||
return Terminal.terminalMap.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive terminal
|
||||
* Mainly used for container exec
|
||||
*/
|
||||
export class InteractiveTerminal extends Terminal {
|
||||
public write(input : string) {
|
||||
this.ptyProcess?.write(input);
|
||||
}
|
||||
|
||||
resetCWD() {
|
||||
const cwd = process.cwd();
|
||||
this.ptyProcess?.write(`cd "${cwd}"\r`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir
|
||||
*/
|
||||
export class MainTerminal extends InteractiveTerminal {
|
||||
constructor(server : DockgeServer, name : string, cwd : string = "./") {
|
||||
let shell;
|
||||
|
||||
if (commandExistsSync("pwsh")) {
|
||||
shell = "pwsh";
|
||||
} else if (os.platform() === "win32") {
|
||||
shell = "powershell.exe";
|
||||
} else {
|
||||
shell = "bash";
|
||||
}
|
||||
super(server, name, shell, [], cwd);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,38 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// For loading dayjs plugins, don't remove event though it is not used in this file
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { randomBytes } from "crypto";
|
||||
let randomBytes : (numBytes: number) => Uint8Array;
|
||||
|
||||
if (typeof window !== "undefined" && window.crypto) {
|
||||
randomBytes = function randomBytes(numBytes: number) {
|
||||
const bytes = new Uint8Array(numBytes);
|
||||
for (let i = 0; i < numBytes; i += 65536) {
|
||||
window.crypto.getRandomValues(bytes.subarray(i, i + Math.min(numBytes - i, 65536)));
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
} else {
|
||||
randomBytes = (await import("node:crypto")).randomBytes;
|
||||
}
|
||||
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
export const TERMINAL_COLS = 80;
|
||||
export const TERMINAL_ROWS = 10;
|
||||
export const PROGRESS_TERMINAL_ROWS = 8;
|
||||
|
||||
export const ERROR_TYPE_VALIDATION = 1;
|
||||
|
||||
export const allowedCommandList : string[] = [
|
||||
"docker",
|
||||
"ls",
|
||||
"cd",
|
||||
"dir",
|
||||
];
|
||||
export const allowedRawKeys = [
|
||||
"\u0003", // Ctrl + C
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a decimal integer number from a string
|
||||
@ -54,7 +80,6 @@ export function genSecret(length = 64) {
|
||||
* @returns Cryptographically suitable random integer
|
||||
*/
|
||||
export function getCryptoRandomInt(min: number, max: number):number {
|
||||
|
||||
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
|
||||
|
||||
const range = max - min;
|
||||
@ -76,11 +101,11 @@ export function getCryptoRandomInt(min: number, max: number):number {
|
||||
tmpRange = tmpRange >>> 1;
|
||||
}
|
||||
|
||||
const randomBytes = getRandomBytes(bytesNeeded);
|
||||
const bytes = randomBytes(bytesNeeded);
|
||||
let randomValue = 0;
|
||||
|
||||
for (let i = 0; i < bytesNeeded; i++) {
|
||||
randomValue |= randomBytes[i] << 8 * i;
|
||||
randomValue |= bytes[i] << 8 * i;
|
||||
}
|
||||
|
||||
randomValue = randomValue & mask;
|
||||
@ -92,27 +117,15 @@ export function getCryptoRandomInt(min: number, max: number):number {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the NodeJS crypto.randomBytes() function or its
|
||||
* browser equivalent implemented via window.crypto.getRandomValues()
|
||||
*/
|
||||
const getRandomBytes = (
|
||||
(typeof window !== "undefined" && window.crypto)
|
||||
|
||||
// Browsers
|
||||
? function () {
|
||||
return (numBytes: number) => {
|
||||
const randomBytes = new Uint8Array(numBytes);
|
||||
for (let i = 0; i < numBytes; i += 65536) {
|
||||
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
|
||||
}
|
||||
return randomBytes;
|
||||
};
|
||||
export function getComposeTerminalName(stack : string) {
|
||||
return "compose-" + stack;
|
||||
}
|
||||
|
||||
// Node
|
||||
: function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
return randomBytes;
|
||||
export function getContainerTerminalName(container : string) {
|
||||
return "container-" + container;
|
||||
}
|
||||
)();
|
||||
|
||||
export function getContainerExecTerminalName(container : string, index : number) {
|
||||
return "container-exec-" + container + "-" + index;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,27 @@
|
||||
import { Socket } from "socket.io";
|
||||
import { Terminal } from "./terminal";
|
||||
import { randomBytes } from "crypto";
|
||||
import { log } from "./log";
|
||||
import { ERROR_TYPE_VALIDATION } from "./util-common";
|
||||
|
||||
export interface DockgeSocket extends Socket {
|
||||
userID: number;
|
||||
consoleTerminal? : Terminal;
|
||||
}
|
||||
|
||||
// For command line arguments, so they are nullable
|
||||
export interface Arguments {
|
||||
sslKey? : string;
|
||||
sslCert? : string;
|
||||
sslKeyPassphrase? : string;
|
||||
port? : number;
|
||||
hostname? : string;
|
||||
dataDir? : string;
|
||||
}
|
||||
|
||||
// Some config values are required
|
||||
export interface Config extends Arguments {
|
||||
dataDir : string;
|
||||
}
|
||||
|
||||
export function checkLogin(socket : DockgeSocket) {
|
||||
@ -9,3 +29,31 @@ export function checkLogin(socket : DockgeSocket) {
|
||||
throw new Error("You are not logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message : string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function callbackError(error : unknown, callback : unknown) {
|
||||
if (typeof(callback) !== "function") {
|
||||
log.error("console", "Callback is not a function");
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
} else if (error instanceof ValidationError) {
|
||||
callback({
|
||||
ok: false,
|
||||
type: ERROR_TYPE_VALIDATION,
|
||||
msg: error.message,
|
||||
});
|
||||
} else {
|
||||
log.debug("console", "Unknown error: " + error);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,39 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM node:20-bookworm-slim
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
# COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
RUN apt update && apt install --yes --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl https://bun.sh/install | bash -s "bun-v1.0.3"
|
||||
dumb-init \
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
||||
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
||||
&& echo \
|
||||
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
|
||||
tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt update \
|
||||
&& apt --yes --no-install-recommends install \
|
||||
docker-ce-cli \
|
||||
docker-compose-plugin \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& npm install pnpm -g \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# ensures that /var/run/docker.sock exists
|
||||
# changes the ownership of /var/run/docker.sock
|
||||
RUN touch /var/run/docker.sock && chown node:node /var/run/docker.sock
|
||||
|
||||
# Full Base Image
|
||||
# MariaDB, Chromium and fonts
|
||||
FROM base-slim AS base
|
||||
ENV DOCKGE_ENABLE_EMBEDDED_MARIADB=1
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install mariadb-server && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
#FROM base-slim AS base
|
||||
#ENV DOCKGE_ENABLE_EMBEDDED_MARIADB=1
|
||||
#RUN apt update && \
|
||||
# apt --yes --no-install-recommends install mariadb-server && \
|
||||
# rm -rf /var/lib/apt/lists/* && \
|
||||
# apt --yes autoremove
|
||||
|
@ -1,15 +1,9 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
RUN apt update && apt install --yes --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl https://bun.sh/install | bash -s "bun-v1.0.3"
|
||||
|
||||
FROM louislam/dockge:base
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN bun install --production --frozen-lockfile
|
||||
COPY --chown=node:node . .
|
||||
RUN pnpm install --prod --frozen-lockfile && \
|
||||
mkdir ./data \
|
||||
VOLUME /app/data
|
||||
EXPOSE 5001
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
CMD ["tsx", "./backend/index.ts"]
|
||||
|
0
docker/compose.yaml
Normal file
0
docker/compose.yaml
Normal file
5
frontend/components.d.ts
vendored
5
frontend/components.d.ts
vendored
@ -7,14 +7,13 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BDropdown: typeof import('bootstrap-vue-next')['BDropdown']
|
||||
BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem']
|
||||
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
||||
Login: typeof import('./src/components/Login.vue')['default']
|
||||
MonitorListItem: typeof import('./src/components/MonitorListItem.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StackList: typeof import('./src/components/StackList.vue')['default']
|
||||
StackListItem: typeof import('./src/components/StackListItem.vue')['default']
|
||||
Terminal: typeof import('./src/components/Terminal.vue')['default']
|
||||
Uptime: typeof import('./src/components/Uptime.vue')['default']
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +107,8 @@ export default {
|
||||
this.$root.loggedIn = true;
|
||||
this.$root.username = this.$root.getJWTPayload()?.username;
|
||||
|
||||
this.$root.afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
<!-- TODO -->
|
||||
<div v-if="false" class="header-filter">
|
||||
<!--<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
<!--<StackListFilter :filterState="filterState" @update-filter="updateFilter" />-->
|
||||
</div>
|
||||
|
||||
<!-- TODO: Selection Controls -->
|
||||
@ -40,12 +40,12 @@
|
||||
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
|
||||
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
|
||||
|
||||
<span v-if="selectedMonitorCount > 0">
|
||||
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
|
||||
<span v-if="selectedStackCount > 0">
|
||||
{{ $t("selectedStackCount", [ selectedStackCount ]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
|
||||
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
|
||||
<div v-if="Object.keys($root.stackList).length === 0" class="text-center mt-3">
|
||||
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
|
||||
</div>
|
||||
@ -53,7 +53,7 @@
|
||||
<StackListItem
|
||||
v-for="(item, index) in sortedStackList"
|
||||
:key="index"
|
||||
:monitor="item"
|
||||
:stack="item"
|
||||
:showPathName="filtersActive"
|
||||
:isSelectMode="selectMode"
|
||||
:isSelected="isSelected"
|
||||
@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
|
||||
{{ $t("pauseMonitorMsg") }}
|
||||
{{ $t("pauseStackMsg") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
@ -89,7 +89,7 @@ export default {
|
||||
selectMode: false,
|
||||
selectAll: false,
|
||||
disableSelectAllWatcher: false,
|
||||
selectedMonitors: {},
|
||||
selectedStacks: {},
|
||||
windowTop: 0,
|
||||
filterState: {
|
||||
status: null,
|
||||
@ -103,7 +103,7 @@ export default {
|
||||
* Improve the sticky appearance of the list by increasing its
|
||||
* height as user scrolls down.
|
||||
* Not used on mobile.
|
||||
* @returns {object} Style for monitor list
|
||||
* @returns {object} Style for stack list
|
||||
*/
|
||||
boxStyle() {
|
||||
if (window.innerWidth > 550) {
|
||||
@ -119,80 +119,43 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a sorted list of monitors based on the applied filters and search text.
|
||||
* @returns {Array} The sorted list of monitors.
|
||||
* Returns a sorted list of stacks based on the applied filters and search text.
|
||||
* @returns {Array} The sorted list of stacks.
|
||||
*/
|
||||
sortedStackList() {
|
||||
let result = Object.values(this.$root.stackList);
|
||||
|
||||
result = result.filter(monitor => {
|
||||
result = result.filter(stack => {
|
||||
// filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
// finds stack name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
stack.name.toLowerCase().includes(loweredSearchText)
|
||||
|| stack.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
|
||||
// filter by status
|
||||
let statusMatch = true;
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
statusMatch = this.filterState.status.includes(monitor.status);
|
||||
}
|
||||
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
activeMatch = this.filterState.active.includes(monitor.active);
|
||||
activeMatch = this.filterState.active.includes(stack.active);
|
||||
}
|
||||
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||
tagsMatch = stack.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(stackTagId => this.filterState.tags.includes(stackTagId)) // perform Array Intersaction between filter and stack's tags
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
// Hide children if not filtering
|
||||
let showChild = true;
|
||||
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
|
||||
if (monitor.parent !== null) {
|
||||
showChild = false;
|
||||
}
|
||||
}
|
||||
|
||||
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
|
||||
return searchTextMatch && activeMatch && tagsMatch;
|
||||
});
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
result.sort((m1, m2) => {
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === false) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
@ -203,8 +166,9 @@ export default {
|
||||
return document.body.classList.contains("dark");
|
||||
},
|
||||
|
||||
monitorListStyle() {
|
||||
let listHeaderHeight = 107;
|
||||
stackListStyle() {
|
||||
//let listHeaderHeight = 107;
|
||||
let listHeaderHeight = 60;
|
||||
|
||||
if (this.selectMode) {
|
||||
listHeaderHeight += 42;
|
||||
@ -215,8 +179,8 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
selectedMonitorCount() {
|
||||
return Object.keys(this.selectedMonitors).length;
|
||||
selectedStackCount() {
|
||||
return Object.keys(this.selectedStacks).length;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -229,8 +193,8 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
searchText() {
|
||||
for (let monitor of this.sortedMonitorList) {
|
||||
if (!this.selectedMonitors[monitor.id]) {
|
||||
for (let stack of this.sortedStackList) {
|
||||
if (!this.selectedStacks[stack.id]) {
|
||||
if (this.selectAll) {
|
||||
this.disableSelectAllWatcher = true;
|
||||
this.selectAll = false;
|
||||
@ -241,11 +205,11 @@ export default {
|
||||
},
|
||||
selectAll() {
|
||||
if (!this.disableSelectAllWatcher) {
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
|
||||
if (this.selectAll) {
|
||||
this.sortedMonitorList.forEach((item) => {
|
||||
this.selectedMonitors[item.id] = true;
|
||||
this.sortedStackList.forEach((item) => {
|
||||
this.selectedStacks[item.id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -255,7 +219,7 @@ export default {
|
||||
selectMode() {
|
||||
if (!this.selectMode) {
|
||||
this.selectAll = false;
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -286,7 +250,7 @@ export default {
|
||||
this.searchText = "";
|
||||
},
|
||||
/**
|
||||
* Update the MonitorList Filter
|
||||
* Update the StackList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
* @returns {void}
|
||||
*/
|
||||
@ -294,28 +258,28 @@ export default {
|
||||
this.filterState = newFilter;
|
||||
},
|
||||
/**
|
||||
* Deselect a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* Deselect a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
deselect(id) {
|
||||
delete this.selectedMonitors[id];
|
||||
delete this.selectedStacks[id];
|
||||
},
|
||||
/**
|
||||
* Select a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* Select a stack
|
||||
* @param {number} id ID of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
select(id) {
|
||||
this.selectedMonitors[id] = true;
|
||||
this.selectedStacks[id] = true;
|
||||
},
|
||||
/**
|
||||
* Determine if monitor is selected
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {bool} Is the monitor selected?
|
||||
* Determine if stack is selected
|
||||
* @param {number} id ID of stack
|
||||
* @returns {bool} Is the stack selected?
|
||||
*/
|
||||
isSelected(id) {
|
||||
return id in this.selectedMonitors;
|
||||
return id in this.selectedStacks;
|
||||
},
|
||||
/**
|
||||
* Disable select mode and reset selection
|
||||
@ -323,7 +287,7 @@ export default {
|
||||
*/
|
||||
cancelSelectMode() {
|
||||
this.selectMode = false;
|
||||
this.selectedMonitors = {};
|
||||
this.selectedStacks = {};
|
||||
},
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
@ -333,24 +297,24 @@ export default {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
/**
|
||||
* Pause each selected monitor
|
||||
* Pause each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseStack", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
/**
|
||||
* Resume each selected monitor
|
||||
* Resume each selected stack
|
||||
* @returns {void}
|
||||
*/
|
||||
resumeSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => !this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
|
||||
Object.keys(this.selectedStacks)
|
||||
.filter(id => !this.$root.stackList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeStack", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
@ -428,7 +392,7 @@ export default {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.monitor-item {
|
||||
.stack-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -7,68 +7,44 @@
|
||||
class="form-check-input select-input"
|
||||
type="checkbox"
|
||||
:aria-label="$t('Check/Uncheck')"
|
||||
:checked="isSelected(monitor.id)"
|
||||
:checked="isSelected(stack.id)"
|
||||
@click.stop="toggleSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
|
||||
<router-link :to="`/compose/${stack.name}`" class="item">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<Uptime :monitor="monitor" type="24" :pill="true" />
|
||||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
||||
</span>
|
||||
{{ monitorName }}
|
||||
<Uptime :stack="stack" type="24" :pill="true" />
|
||||
{{ stackName }}
|
||||
</div>
|
||||
<div v-if="monitor.tags.length > 0" class="tags">
|
||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
<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 v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12 bottom-style">
|
||||
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<transition name="slide-fade-up">
|
||||
<div v-if="!isCollapsed" class="childs">
|
||||
<MonitorListItem
|
||||
v-for="(item, index) in sortedChildMonitorList"
|
||||
:key="index" :monitor="item"
|
||||
:showPathName="showPathName"
|
||||
:isSelectMode="isSelectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
:deselect="deselect"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Uptime from "./Uptime.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime
|
||||
|
||||
},
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
/** Stack this represents */
|
||||
stack: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Should the monitor name show it's parent */
|
||||
/** Should the stack name show it's parent */
|
||||
showPathName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -78,22 +54,22 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** How many ancestors are above this monitor */
|
||||
/** How many ancestors are above this stack */
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/** Callback to determine if monitor is selected */
|
||||
/** Callback to determine if stack is selected */
|
||||
isSelected: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Callback fired when monitor is selected */
|
||||
/** Callback fired when stack is selected */
|
||||
select: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Callback fired when monitor is deselected */
|
||||
/** Callback fired when stack is deselected */
|
||||
deselect: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
@ -105,51 +81,16 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedChildMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
hasChildren() {
|
||||
return this.sortedChildMonitorList.length > 0;
|
||||
},
|
||||
depthMargin() {
|
||||
return {
|
||||
marginLeft: `${31 * this.depth}px`,
|
||||
};
|
||||
},
|
||||
monitorName() {
|
||||
stackName() {
|
||||
if (this.showPathName) {
|
||||
return this.monitor.pathName;
|
||||
return this.stack.pathName;
|
||||
} else {
|
||||
return this.monitor.name;
|
||||
return this.stack.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -161,28 +102,10 @@ export default {
|
||||
},
|
||||
beforeMount() {
|
||||
|
||||
// Always unfold if monitor is accessed directly
|
||||
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
|
||||
this.isCollapsed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set collapsed value based on local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
if (storage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let storageObject = JSON.parse(storage);
|
||||
if (storageObject[`monitor_${this.monitor.id}`] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Changes the collapsed value of the current monitor and saves
|
||||
* Changes the collapsed value of the current stack and saves
|
||||
* it to local storage
|
||||
* @returns {void}
|
||||
*/
|
||||
@ -190,25 +113,25 @@ export default {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
// Save collapsed value into local storage
|
||||
let storage = window.localStorage.getItem("monitorCollapsed");
|
||||
let storage = window.localStorage.getItem("stackCollapsed");
|
||||
let storageObject = {};
|
||||
if (storage !== null) {
|
||||
storageObject = JSON.parse(storage);
|
||||
}
|
||||
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
|
||||
storageObject[`stack_${this.stack.id}`] = this.isCollapsed;
|
||||
|
||||
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
|
||||
window.localStorage.setItem("stackCollapsed", JSON.stringify(storageObject));
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle selection of monitor
|
||||
* Toggle selection of stack
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleSelection() {
|
||||
if (this.isSelected(this.monitor.id)) {
|
||||
this.deselect(this.monitor.id);
|
||||
if (this.isSelected(this.stack.id)) {
|
||||
this.deselect(this.stack.id);
|
||||
} else {
|
||||
this.select(this.monitor.id);
|
||||
this.select(this.stack.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -228,7 +151,7 @@ export default {
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
|
||||
// .monitor-item {
|
||||
// .stack-item {
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
|
93
frontend/src/components/Terminal.vue
Normal file
93
frontend/src/components/Terminal.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="shadow-box">
|
||||
<div v-pre ref="terminal" class="main-terminal"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Terminal } from "xterm";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
|
||||
|
||||
export default {
|
||||
/**
|
||||
* @type {Terminal}
|
||||
*/
|
||||
terminal: null,
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
allowInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: TERMINAL_ROWS,
|
||||
}
|
||||
},
|
||||
emits: [ "has-data" ],
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
first: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.terminal = new Terminal({
|
||||
fontSize: 16,
|
||||
fontFamily: "monospace",
|
||||
cursorBlink: this.allowInput,
|
||||
cols: TERMINAL_COLS,
|
||||
rows: this.rows,
|
||||
});
|
||||
|
||||
this.terminal.loadAddon(new WebLinksAddon());
|
||||
|
||||
// Bind to a div
|
||||
this.terminal.open(this.$refs.terminal);
|
||||
this.terminal.focus();
|
||||
|
||||
// Notify parent component when data is received
|
||||
this.terminal.onCursorMove(() => {
|
||||
console.debug("onData triggered");
|
||||
if (this.first) {
|
||||
this.$emit("has-data");
|
||||
this.first = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.$root.unbindTerminal(this.name);
|
||||
this.terminal.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
bind(name) {
|
||||
if (this.name) {
|
||||
this.$root.unbindTerminal(this.name);
|
||||
}
|
||||
this.name = name;
|
||||
this.$root.bindTerminal(this.name, this.terminal);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.main-terminal {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.terminal {
|
||||
padding: 10px 15px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
58
frontend/src/components/Uptime.vue
Normal file
58
frontend/src/components/Uptime.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<span :class="className" :title="title">{{ uptime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/** Monitor this represents */
|
||||
monitor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Type of monitor */
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/** Is this a pill? */
|
||||
pill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
uptime() {
|
||||
return this.$t("notAvailableShort");
|
||||
},
|
||||
|
||||
color() {
|
||||
return "secondary";
|
||||
},
|
||||
|
||||
className() {
|
||||
if (this.pill) {
|
||||
return `badge rounded-pill bg-${this.color}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
title() {
|
||||
if (this.type === "720") {
|
||||
return `30${this.$t("-day")}`;
|
||||
}
|
||||
|
||||
return `24${this.$t("-hour")}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
min-width: 62px;
|
||||
}
|
||||
</style>
|
@ -50,7 +50,7 @@ import {
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
faCertificate,
|
||||
faTerminal, faWarehouse, faHome,
|
||||
faTerminal, faWarehouse, faHome, faRocket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@ -101,6 +101,7 @@ library.add(
|
||||
faTerminal,
|
||||
faWarehouse,
|
||||
faHome,
|
||||
faRocket,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
@ -8,5 +8,15 @@
|
||||
"console": "Console",
|
||||
"registry": "Registry",
|
||||
"compose": "Compose",
|
||||
"addFirstStackMsg": "Compose your first stack!"
|
||||
"addFirstStackMsg": "Compose your first stack!",
|
||||
"stackName" : "Stack Name",
|
||||
"deployStack": "Deploy",
|
||||
"deleteStack": "Delete",
|
||||
"stopStack": "Stop",
|
||||
"restartStack": "Restart",
|
||||
"startStack": "Start",
|
||||
"editStack": "Edit",
|
||||
"discardStack": "Discard",
|
||||
"saveStackDraft": "Save",
|
||||
"notAvailableShort" : "N/A"
|
||||
}
|
||||
|
@ -3,20 +3,13 @@ import { Socket } from "socket.io-client";
|
||||
import { defineComponent } from "vue";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
|
||||
const terminal = new Terminal({
|
||||
fontSize: 16,
|
||||
fontFamily: "monospace",
|
||||
cursorBlink: true,
|
||||
});
|
||||
terminal.loadAddon(new FitAddon());
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
let terminalInputBuffer = "";
|
||||
let cursorPosition = 0;
|
||||
let socket : Socket;
|
||||
|
||||
let terminalMap : Map<string, Terminal> = new Map();
|
||||
|
||||
function removeInput() {
|
||||
const backspaceCount = terminalInputBuffer.length;
|
||||
const backspaces = "\b \b".repeat(backspaceCount);
|
||||
@ -44,7 +37,6 @@ export default defineComponent({
|
||||
loggedIn: false,
|
||||
allowLoginDialog: false,
|
||||
username: null,
|
||||
|
||||
stackList: {},
|
||||
};
|
||||
},
|
||||
@ -66,6 +58,7 @@ export default defineComponent({
|
||||
this.initSocketIO();
|
||||
},
|
||||
mounted() {
|
||||
return;
|
||||
terminal.onKey(e => {
|
||||
const code = e.key.charCodeAt(0);
|
||||
console.debug("Encode: " + JSON.stringify(e.key));
|
||||
@ -100,6 +93,7 @@ export default defineComponent({
|
||||
// TODO
|
||||
} else if (e.key === "\u0003") { // Ctrl + C
|
||||
console.debug("Ctrl + C");
|
||||
socket.emit("terminalInputRaw", e.key);
|
||||
removeInput();
|
||||
} else {
|
||||
cursorPosition++;
|
||||
@ -131,7 +125,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
socket = io(url, {
|
||||
|
||||
transports: [ "websocket", "polling" ]
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
@ -195,9 +189,20 @@ export default defineComponent({
|
||||
this.$router.push("/setup");
|
||||
});
|
||||
|
||||
socket.on("commandOutput", (data) => {
|
||||
socket.on("terminalWrite", (terminalName, data) => {
|
||||
const terminal = terminalMap.get(terminalName);
|
||||
if (!terminal) {
|
||||
console.error("Terminal not found: " + terminalName);
|
||||
return;
|
||||
}
|
||||
terminal.write(data);
|
||||
});
|
||||
|
||||
socket.on("stackList", (res) => {
|
||||
if (res.ok) {
|
||||
this.stackList = res.stackList;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -212,10 +217,6 @@ export default defineComponent({
|
||||
return socket;
|
||||
},
|
||||
|
||||
getTerminal() : Terminal {
|
||||
return terminal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get payload of JWT cookie
|
||||
* @returns {(object | undefined)} JWT payload
|
||||
@ -269,13 +270,23 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
afterLogin() {
|
||||
terminal.clear();
|
||||
|
||||
},
|
||||
|
||||
bindTerminal(terminalName : string, terminal : Terminal) {
|
||||
// Load terminal, get terminal screen
|
||||
socket.emit("getTerminalBuffer", (res) => {
|
||||
console.log("getTerminalBuffer");
|
||||
socket.emit("terminalJoin", terminalName, (res) => {
|
||||
if (res.ok) {
|
||||
terminal.write(res.buffer);
|
||||
terminalMap.set(terminalName, terminal);
|
||||
} else {
|
||||
this.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unbindTerminal(terminalName : string) {
|
||||
terminalMap.delete(terminalName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,16 +1,224 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 v-if="isAdd" class="mb-3">Compose</h1>
|
||||
<h1 v-else class="mb-3">Stack: {{ stack.name }}</h1>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
|
||||
<font-awesome-icon icon="rocket" class="me-1" />
|
||||
{{ $t("deployStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="isEditMode" class="btn btn-normal" :disabled="processing" @click="saveStack">
|
||||
<font-awesome-icon icon="save" class="me-1" />
|
||||
{{ $t("saveStackDraft") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
|
||||
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-primary " :disabled="processing">{{ $t("restartStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("stopStack") }}</button>
|
||||
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("deleteStack") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Terminal -->
|
||||
<transition name="slide-fade" appear>
|
||||
<Terminal
|
||||
v-show="showProgressTerminal"
|
||||
ref="progressTerminal"
|
||||
:allow-input="false"
|
||||
class="mb-3 terminal"
|
||||
:rows="progressTerminalRows"
|
||||
@has-data="showProgressTerminal = true"
|
||||
></Terminal>
|
||||
</transition>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="mb-3">General</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<!-- Stack Name -->
|
||||
<div v-if="isAdd" class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
||||
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mb-3">Containers</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<div v-for="(service, name) in jsonConfig.services" :key="name">
|
||||
{{ name }} {{ service }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4 class="mb-3">compose.yaml</h4>
|
||||
|
||||
<div class="shadow-box mb-3">
|
||||
<prism-editor v-model="stack.composeYAML" class="yaml-editor" :highlight="highlighter" line-numbers :readonly="!isEditMode" @input="yamlCodeChange"></prism-editor>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ yamlError }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="shadow-box big-padding mb-3">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label"> Search Templates</label>
|
||||
<input id="name" v-model="name" type="text" class="form-control" placeholder="Search..." required>
|
||||
</div>
|
||||
|
||||
<prism-editor v-if="false" v-model="yamlConfig" class="yaml-editor" :highlight="highlighter" line-numbers @input="yamlCodeChange"></prism-editor>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
<script>
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import * as yaml from "yaml";
|
||||
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "../../../backend/util-common";
|
||||
|
||||
const template = `version: "3.8"
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
`;
|
||||
|
||||
let yamlErrorTimeout = null;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
PrismEditor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonConfig: {},
|
||||
yamlError: "",
|
||||
processing: true,
|
||||
showProgressTerminal: false,
|
||||
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
|
||||
stack: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isAdd() {
|
||||
return this.$route.path === "/compose" && !this.submitted;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"stack.composeYAML": {
|
||||
handler() {
|
||||
this.yamlCodeChange();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isAdd) {
|
||||
this.processing = false;
|
||||
this.isEditMode = true;
|
||||
|
||||
// Default Values
|
||||
this.stack = {
|
||||
name: "",
|
||||
composeYAML: template,
|
||||
};
|
||||
|
||||
} else {
|
||||
this.stack.name = this.$route.params.stackName;
|
||||
this.loadStack();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadStack() {
|
||||
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.stack = res.stack;
|
||||
this.processing = false;
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
deployStack() {
|
||||
this.processing = true;
|
||||
|
||||
// Bind Terminal output
|
||||
const terminalName = getComposeTerminalName(this.stack.name);
|
||||
this.$refs.progressTerminal.bind(terminalName);
|
||||
|
||||
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
} else {
|
||||
this.submitted = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
saveStack() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.$router.push("/compose/" + this.stack.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
discardStack() {
|
||||
this.loadStack();
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
highlighter(code) {
|
||||
return highlight(code, languages.yaml);
|
||||
},
|
||||
yamlCodeChange() {
|
||||
try {
|
||||
this.jsonConfig = yaml.parse(this.stack.composeYAML) ?? {};
|
||||
this.yamlError = "";
|
||||
} catch (e) {
|
||||
clearTimeout(yamlErrorTimeout);
|
||||
|
||||
if (this.yamlError) {
|
||||
this.yamlError = e.message;
|
||||
|
||||
} else {
|
||||
yamlErrorTimeout = setTimeout(() => {
|
||||
this.yamlError = e.message;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.terminal {
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,9 +3,13 @@
|
||||
<div>
|
||||
<h1 class="mb-3">Console</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div v-pre id="terminal"></div>
|
||||
<div>
|
||||
<p>
|
||||
Allowed commands: <code>docker</code>, <code>ls</code>, <code>cd</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Terminal :allow-input="true" class="terminal"></Terminal>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@ -14,10 +18,9 @@
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getTerminal().open(document.querySelector("#terminal"));
|
||||
this.$root.terminalFit(50);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -27,9 +30,6 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.terminal {
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
padding: 10px 15px;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -4,8 +4,8 @@ import Layout from "./layouts/Layout.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
import Dashboard from "./pages/Dashboard.vue";
|
||||
import DashboardHome from "./pages/DashboardHome.vue";
|
||||
import EditStack from "./pages/EditStack.vue";
|
||||
import Console from "./pages/Console.vue";
|
||||
import Compose from "./pages/Compose.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -23,7 +23,13 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: "/compose",
|
||||
component: EditStack,
|
||||
component: Compose,
|
||||
},
|
||||
{
|
||||
path: "/compose/:stackName",
|
||||
name: "compose",
|
||||
component: Compose,
|
||||
props: true,
|
||||
},
|
||||
|
||||
]
|
||||
|
@ -377,7 +377,7 @@ optgroup {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.stack-list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
@ -474,7 +474,7 @@ optgroup {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monitor-list {
|
||||
.stack-list {
|
||||
&.scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -653,12 +653,28 @@ $shadow-box-padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#terminal {
|
||||
.main-terminal {
|
||||
.xterm-viewport {
|
||||
border-radius: 10px;
|
||||
background-color: $dark-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
background-color: rgba(239, 239, 239, 0.15);
|
||||
|
||||
border-radius: 6px;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
color: black;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Localization
|
||||
@import "localization.scss";
|
||||
|
@ -158,7 +158,7 @@ export function colorOptions(self) {
|
||||
export function loadToastSettings() {
|
||||
return {
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
containerClassName: "toast-container mb-5",
|
||||
containerClassName: "toast-container",
|
||||
showCloseButtonOnHover: true,
|
||||
|
||||
filterBeforeCreate: (toast, toasts) => {
|
||||
|
@ -14,7 +14,7 @@ export default defineConfig({
|
||||
},
|
||||
root: "./frontend",
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
outDir: "../frontend-dist",
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
|
31
package.json
31
package.json
@ -5,27 +5,33 @@
|
||||
"scripts": {
|
||||
"fmt": "eslint \"**/*.{ts,vue}\" --fix",
|
||||
"lint": "eslint \"**/*.{ts,vue}\"",
|
||||
"start": "tsx ./backend/index.ts",
|
||||
"dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts",
|
||||
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
|
||||
"build:frontend": "vite build --config ./frontend/vite.config.ts"
|
||||
"build:frontend": "vite build --config ./frontend/vite.config.ts",
|
||||
"build:docker-base": "docker build -t louislam/dockge:base -f ./docker/Base.Dockerfile .",
|
||||
"build:docker": "pnpm run build:frontend && docker build -t louislam/dockge:latest -f ./docker/Dockerfile .",
|
||||
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.7",
|
||||
"@louislam/sqlite3": "~15.1.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"check-password-strength": "^2.0.7",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"check-password-strength": "~2.0.7",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "~6.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "~4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"jsonwebtoken": "~9.0.2",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"knex": "~2.5.1",
|
||||
"limiter-es6-compat": "~2.1.2",
|
||||
"mysql2": "^3.6.2",
|
||||
"node-pty": "^1.0.0",
|
||||
"redbean-node": "0.3.1",
|
||||
"socket.io": "~4.7.2",
|
||||
"socket.io-client": "~4.7.2",
|
||||
"timezones-list": "^3.0.2",
|
||||
"timezones-list": "~3.0.2",
|
||||
"ts-command-line-args": "~2.5.1",
|
||||
"tsx": "~3.14.0",
|
||||
"type-fest": "~4.3.3",
|
||||
@ -39,7 +45,9 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@types/command-exists": "~1.2.2",
|
||||
"@types/express": "~4.17.20",
|
||||
"@types/jsonwebtoken": "~9.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
||||
"@typescript-eslint/parser": "~6.8.0",
|
||||
"@vitejs/plugin-vue": "~4.3.4",
|
||||
@ -49,6 +57,9 @@
|
||||
"eslint": "~8.50.0",
|
||||
"eslint-plugin-jsdoc": "~46.8.2",
|
||||
"eslint-plugin-vue": "~9.17.0",
|
||||
"monaco-editor": "^0.44.0",
|
||||
"monaco-yaml": "^5.1.0",
|
||||
"prismjs": "~1.29.0",
|
||||
"sass": "~1.68.0",
|
||||
"typescript": "~5.2.2",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
@ -57,10 +68,10 @@
|
||||
"vue": "~3.3.6",
|
||||
"vue-eslint-parser": "^9.3.2",
|
||||
"vue-i18n": "~9.5.0",
|
||||
"vue-prism-editor": "~2.0.0-alpha.2",
|
||||
"vue-router": "~4.2.5",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-web-links": "^0.9.0"
|
||||
"xterm": "~5.3.0",
|
||||
"xterm-addon-web-links": "~0.9.0"
|
||||
}
|
||||
}
|
||||
|
353
pnpm-lock.yaml
353
pnpm-lock.yaml
@ -5,15 +5,21 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@homebridge/node-pty-prebuilt-multiarch':
|
||||
specifier: ~0.11.7
|
||||
version: 0.11.7
|
||||
'@louislam/sqlite3':
|
||||
specifier: ~15.1.6
|
||||
version: 15.1.6
|
||||
bcryptjs:
|
||||
specifier: ^2.4.3
|
||||
specifier: ~2.4.3
|
||||
version: 2.4.3
|
||||
check-password-strength:
|
||||
specifier: ^2.0.7
|
||||
specifier: ~2.0.7
|
||||
version: 2.0.7
|
||||
command-exists:
|
||||
specifier: ^1.2.9
|
||||
version: 1.2.9
|
||||
compare-versions:
|
||||
specifier: ~6.1.0
|
||||
version: 6.1.0
|
||||
@ -23,11 +29,14 @@ dependencies:
|
||||
express:
|
||||
specifier: ~4.18.2
|
||||
version: 4.18.2
|
||||
express-static-gzip:
|
||||
specifier: ~2.1.7
|
||||
version: 2.1.7
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
specifier: ~9.0.2
|
||||
version: 9.0.2
|
||||
jwt-decode:
|
||||
specifier: ^3.1.2
|
||||
specifier: ~3.1.2
|
||||
version: 3.1.2
|
||||
knex:
|
||||
specifier: ~2.5.1
|
||||
@ -38,9 +47,6 @@ dependencies:
|
||||
mysql2:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
node-pty:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
redbean-node:
|
||||
specifier: 0.3.1
|
||||
version: 0.3.1(mysql2@3.6.2)
|
||||
@ -51,7 +57,7 @@ dependencies:
|
||||
specifier: ~4.7.2
|
||||
version: 4.7.2
|
||||
timezones-list:
|
||||
specifier: ^3.0.2
|
||||
specifier: ~3.0.2
|
||||
version: 3.0.2
|
||||
ts-command-line-args:
|
||||
specifier: ~2.5.1
|
||||
@ -84,9 +90,15 @@ devDependencies:
|
||||
'@fortawesome/vue-fontawesome':
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.6)
|
||||
'@types/command-exists':
|
||||
specifier: ~1.2.2
|
||||
version: 1.2.2
|
||||
'@types/express':
|
||||
specifier: ~4.17.20
|
||||
version: 4.17.20
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ~9.0.4
|
||||
version: 9.0.4
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ~6.8.0
|
||||
version: 6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.50.0)(typescript@5.2.2)
|
||||
@ -114,6 +126,15 @@ devDependencies:
|
||||
eslint-plugin-vue:
|
||||
specifier: ~9.17.0
|
||||
version: 9.17.0(eslint@8.50.0)
|
||||
monaco-editor:
|
||||
specifier: ^0.44.0
|
||||
version: 0.44.0
|
||||
monaco-yaml:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(monaco-editor@0.44.0)
|
||||
prismjs:
|
||||
specifier: ~1.29.0
|
||||
version: 1.29.0
|
||||
sass:
|
||||
specifier: ~1.68.0
|
||||
version: 1.68.0
|
||||
@ -138,6 +159,9 @@ devDependencies:
|
||||
vue-i18n:
|
||||
specifier: ~9.5.0
|
||||
version: 9.5.0(vue@3.3.6)
|
||||
vue-prism-editor:
|
||||
specifier: ~2.0.0-alpha.2
|
||||
version: 2.0.0-alpha.2(vue@3.3.6)
|
||||
vue-router:
|
||||
specifier: ~4.2.5
|
||||
version: 4.2.5(vue@3.3.6)
|
||||
@ -145,13 +169,10 @@ devDependencies:
|
||||
specifier: ~2.0.0-rc.5
|
||||
version: 2.0.0-rc.5(vue@3.3.6)
|
||||
xterm:
|
||||
specifier: ^5.3.0
|
||||
specifier: ~5.3.0
|
||||
version: 5.3.0
|
||||
xterm-addon-fit:
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(xterm@5.3.0)
|
||||
xterm-addon-web-links:
|
||||
specifier: ^0.9.0
|
||||
specifier: ~0.9.0
|
||||
version: 0.9.0(xterm@5.3.0)
|
||||
|
||||
packages:
|
||||
@ -487,6 +508,14 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@homebridge/node-pty-prebuilt-multiarch@0.11.7:
|
||||
resolution: {integrity: sha512-aaw66RDwHZ2Xs821U6hwEl2wPDyv9PAWEzNxS32YSRaoYmbie1AREehAfjbASGgpub+7d+3l98xft3oCCUKhSw==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
nan: 2.18.0
|
||||
prebuild-install: 7.1.1
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.13:
|
||||
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@ -777,6 +806,10 @@ packages:
|
||||
'@types/node': 20.8.7
|
||||
dev: true
|
||||
|
||||
/@types/command-exists@1.2.2:
|
||||
resolution: {integrity: sha512-1qKPTkjLmghE5C7UUHXGcLaG8MNftchOcLAIryUXNKahRO5beS+iJ9rIL8XD4+B8K2phjYUsPQDox1FRX4KMTQ==}
|
||||
dev: true
|
||||
|
||||
/@types/connect@3.4.37:
|
||||
resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==}
|
||||
dependencies:
|
||||
@ -823,6 +856,12 @@ packages:
|
||||
resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==}
|
||||
dev: true
|
||||
|
||||
/@types/jsonwebtoken@9.0.4:
|
||||
resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.7
|
||||
dev: true
|
||||
|
||||
/@types/mime@1.3.4:
|
||||
resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==}
|
||||
dev: true
|
||||
@ -1363,6 +1402,10 @@ packages:
|
||||
/balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: false
|
||||
|
||||
/base64id@2.0.0:
|
||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||
engines: {node: ^4.5.0 || >= 5.9}
|
||||
@ -1383,6 +1426,14 @@ packages:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: false
|
||||
|
||||
/blessed@0.1.81:
|
||||
resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -1466,6 +1517,13 @@ packages:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
dev: false
|
||||
|
||||
/buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
dev: false
|
||||
|
||||
/builtin-modules@3.3.0:
|
||||
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1567,6 +1625,10 @@ packages:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
/chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: false
|
||||
|
||||
/chownr@2.0.0:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1616,6 +1678,10 @@ packages:
|
||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
dev: false
|
||||
|
||||
/command-exists@1.2.9:
|
||||
resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==}
|
||||
dev: false
|
||||
|
||||
/command-line-args@5.2.1:
|
||||
resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@ -1801,6 +1867,13 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
dev: false
|
||||
|
||||
/deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@ -1912,6 +1985,12 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/end-of-stream@1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/engine.io-client@6.5.2:
|
||||
resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==}
|
||||
dependencies:
|
||||
@ -2202,6 +2281,19 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/express-static-gzip@2.1.7:
|
||||
resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==}
|
||||
dependencies:
|
||||
serve-static: 1.15.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/express@4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@ -2370,6 +2462,10 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
dev: false
|
||||
|
||||
/fs-extra@10.1.0:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2505,6 +2601,10 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
dev: false
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -2695,6 +2795,10 @@ packages:
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: false
|
||||
|
||||
/ignore@5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -2743,7 +2847,6 @@ packages:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/interpret@2.2.0:
|
||||
resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
|
||||
@ -2874,6 +2977,10 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/jsonc-parser@3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
dev: true
|
||||
|
||||
/jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
requiresBuild: true
|
||||
@ -3219,6 +3326,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
@ -3230,6 +3342,10 @@ packages:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
/minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: false
|
||||
|
||||
/minipass-collect@1.0.2:
|
||||
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -3304,6 +3420,10 @@ packages:
|
||||
yallist: 4.0.0
|
||||
dev: false
|
||||
|
||||
/mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
dev: false
|
||||
|
||||
/mkdirp@1.0.4:
|
||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -3316,6 +3436,57 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/monaco-editor@0.44.0:
|
||||
resolution: {integrity: sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==}
|
||||
dev: true
|
||||
|
||||
/monaco-languageserver-types@0.2.3:
|
||||
resolution: {integrity: sha512-QyV5R7s+rJ87bX1sRioMJZULWiTnMp0Vm+RLILgMEL0SqWuBsQBSW0EZunr4xMZhv6Qun3UZNCN4JrCCLURcgQ==}
|
||||
dependencies:
|
||||
monaco-types: 0.1.0
|
||||
vscode-languageserver-protocol: 3.17.5
|
||||
dev: true
|
||||
|
||||
/monaco-marker-data-provider@1.1.1(monaco-editor@0.44.0):
|
||||
resolution: {integrity: sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==}
|
||||
peerDependencies:
|
||||
monaco-editor: '>=0.30.0'
|
||||
dependencies:
|
||||
monaco-editor: 0.44.0
|
||||
dev: true
|
||||
|
||||
/monaco-types@0.1.0:
|
||||
resolution: {integrity: sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==}
|
||||
dev: true
|
||||
|
||||
/monaco-worker-manager@2.0.1(monaco-editor@0.44.0):
|
||||
resolution: {integrity: sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==}
|
||||
peerDependencies:
|
||||
monaco-editor: '>=0.30.0'
|
||||
dependencies:
|
||||
monaco-editor: 0.44.0
|
||||
dev: true
|
||||
|
||||
/monaco-yaml@5.1.0(monaco-editor@0.44.0):
|
||||
resolution: {integrity: sha512-DU+cgXSJdOFKQ4I4oLg0V+mHKq1dJX+7hbIE4fJsgegUf1zEHW3PNlGj6qabUU2HZIPJ5NmXEf005GU9YDzTYQ==}
|
||||
peerDependencies:
|
||||
monaco-editor: '>=0.36'
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.14
|
||||
jsonc-parser: 3.2.0
|
||||
monaco-editor: 0.44.0
|
||||
monaco-languageserver-types: 0.2.3
|
||||
monaco-marker-data-provider: 1.1.1(monaco-editor@0.44.0)
|
||||
monaco-types: 0.1.0
|
||||
monaco-worker-manager: 2.0.1(monaco-editor@0.44.0)
|
||||
path-browserify: 1.0.1
|
||||
prettier: 2.8.8
|
||||
vscode-languageserver-textdocument: 1.0.11
|
||||
vscode-languageserver-types: 3.17.5
|
||||
vscode-uri: 3.0.8
|
||||
yaml: 2.3.3
|
||||
dev: true
|
||||
|
||||
/ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: false
|
||||
@ -3364,6 +3535,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/napi-build-utils@1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: false
|
||||
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: true
|
||||
@ -3395,6 +3570,13 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/node-abi@3.51.0:
|
||||
resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
dev: false
|
||||
|
||||
/node-addon-api@4.3.0:
|
||||
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
|
||||
dev: false
|
||||
@ -3433,13 +3615,6 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/node-pty@1.0.0:
|
||||
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
nan: 2.18.0
|
||||
dev: false
|
||||
|
||||
/nopt@5.0.0:
|
||||
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||
engines: {node: '>=6'}
|
||||
@ -3593,6 +3768,10 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
dev: true
|
||||
|
||||
/path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3779,11 +3958,41 @@ packages:
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/prebuild-install@7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
detect-libc: 2.0.2
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.51.0
|
||||
pump: 3.0.0
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/prettier@2.8.8:
|
||||
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/promise-inflight@1.0.1:
|
||||
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
||||
requiresBuild: true
|
||||
@ -3845,6 +4054,13 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/pump@3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/punycode@2.3.0:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -3876,6 +4092,16 @@ packages:
|
||||
unpipe: 1.0.0
|
||||
dev: false
|
||||
|
||||
/rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/read@1.0.7:
|
||||
resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==}
|
||||
engines: {node: '>=0.8'}
|
||||
@ -4136,6 +4362,18 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
dev: false
|
||||
|
||||
/simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
dev: false
|
||||
|
||||
/simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
dev: false
|
||||
|
||||
/slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4339,6 +4577,11 @@ packages:
|
||||
ansi-regex: 6.0.1
|
||||
dev: false
|
||||
|
||||
/strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4380,6 +4623,26 @@ packages:
|
||||
wordwrapjs: 4.0.1
|
||||
dev: false
|
||||
|
||||
/tar-fs@2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 2.2.0
|
||||
dev: false
|
||||
|
||||
/tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.4
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: false
|
||||
|
||||
/tar@6.2.0:
|
||||
resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4472,6 +4735,12 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: false
|
||||
|
||||
/tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/tv4@1.3.0:
|
||||
resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -4690,6 +4959,30 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/vscode-jsonrpc@8.2.0:
|
||||
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/vscode-languageserver-protocol@3.17.5:
|
||||
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
|
||||
dependencies:
|
||||
vscode-jsonrpc: 8.2.0
|
||||
vscode-languageserver-types: 3.17.5
|
||||
dev: true
|
||||
|
||||
/vscode-languageserver-textdocument@1.0.11:
|
||||
resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==}
|
||||
dev: true
|
||||
|
||||
/vscode-languageserver-types@3.17.5:
|
||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
||||
dev: true
|
||||
|
||||
/vscode-uri@3.0.8:
|
||||
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
|
||||
dev: true
|
||||
|
||||
/vue-demi@0.14.6(vue@3.3.6):
|
||||
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
||||
engines: {node: '>=12'}
|
||||
@ -4735,6 +5028,15 @@ packages:
|
||||
vue: 3.3.6(typescript@5.2.2)
|
||||
dev: true
|
||||
|
||||
/vue-prism-editor@2.0.0-alpha.2(vue@3.3.6):
|
||||
resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
dependencies:
|
||||
vue: 3.3.6(typescript@5.2.2)
|
||||
dev: true
|
||||
|
||||
/vue-router@4.2.5(vue@3.3.6):
|
||||
resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
|
||||
peerDependencies:
|
||||
@ -4884,14 +5186,6 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/xterm-addon-fit@0.8.0(xterm@5.3.0):
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
dependencies:
|
||||
xterm: 5.3.0
|
||||
dev: true
|
||||
|
||||
/xterm-addon-web-links@0.9.0(xterm@5.3.0):
|
||||
resolution: {integrity: sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==}
|
||||
peerDependencies:
|
||||
@ -4910,7 +5204,6 @@ packages:
|
||||
/yaml@2.3.3:
|
||||
resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/yamljs@0.3.0:
|
||||
resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==}
|
||||
|
Loading…
Reference in New Issue
Block a user