forked from extern/dockge
Compare commits
36 Commits
env-follow
...
compose
Author | SHA1 | Date | |
---|---|---|---|
7ed045af05 | |||
7581e61153 | |||
3c137122b6 | |||
e2819afce1 | |||
94ca8a152a | |||
db0add7e4c | |||
0f52bb78b8 | |||
03c7815b58 | |||
80d5c685e5 | |||
a8482ec8ac | |||
07c52ccebb | |||
587d2dcaca | |||
5f1f3593fd | |||
316c566c76 | |||
d32bd3937f | |||
007eac7b58 | |||
b945ddea55 | |||
9b6b49947c | |||
3ba267a3dc | |||
01411f2d7e | |||
ba51031db6 | |||
daa8d12eee | |||
bd58de535e | |||
8c6bcef987 | |||
bec5460395 | |||
4e899dcf21 | |||
8296c7b18f | |||
607c908f2d | |||
bd5dd3c3ad | |||
6eca6dc59f | |||
54fb2c1ef4 | |||
562abb485d | |||
86bed768ea | |||
b79db2375f | |||
b586cca711 | |||
793a9de50d |
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
|
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
|
||||||
|
|
||||||
      
|
   
|
||||||
|
|
||||||
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
|
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ Be sure to read the [guide](https://github.com/louislam/dockge/blob/master/CONTR
|
|||||||
|
|
||||||
#### "Dockge"?
|
#### "Dockge"?
|
||||||
|
|
||||||
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Dodge`.
|
"Dockge" is a coinage word which is created by myself. I originally hoped it sounds like `Dodge`, but apparently many people called it `Dockage`, it is also acceptable.
|
||||||
|
|
||||||
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`.
|
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`.
|
||||||
|
|
||||||
@ -148,13 +148,13 @@ Yes, you can. However, you need to move your compose file into the stacks direct
|
|||||||
3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
|
3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
|
||||||
4. Now you should see your stack in the list
|
4. Now you should see your stack in the list
|
||||||
|
|
||||||
#### Is Dockge a Portainer replcement?
|
#### Is Dockge a Portainer replacement?
|
||||||
|
|
||||||
Yes or no. Portainer provides a lot of Docker features. While Dockge is currently only focusing on docker-compose with a better user interface and better user experience.
|
Yes or no. Portainer provides a lot of Docker features. While Dockge is currently only focusing on docker-compose with a better user interface and better user experience.
|
||||||
|
|
||||||
If you want to manage your container with docker-compose only, the answer may be yes.
|
If you want to manage your container with docker-compose only, the answer may be yes.
|
||||||
|
|
||||||
If you still need to manage something like docker networks, signle containers, the answer may be no.
|
If you still need to manage something like docker networks, single containers, the answer may be no.
|
||||||
|
|
||||||
#### Can I install both Dockge and Portainer?
|
#### Can I install both Dockge and Portainer?
|
||||||
|
|
||||||
|
@ -3,69 +3,55 @@ import compareVersions from "compare-versions";
|
|||||||
import packageJSON from "../package.json";
|
import packageJSON from "../package.json";
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
|
|
||||||
export const obj = {
|
|
||||||
version: packageJSON.version,
|
|
||||||
latestVersion: null,
|
|
||||||
};
|
|
||||||
export default obj;
|
|
||||||
|
|
||||||
// How much time in ms to wait between update checks
|
// How much time in ms to wait between update checks
|
||||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
||||||
const CHECK_URL = "https://dockge.kuma.pet/version";
|
const CHECK_URL = "https://dockge.kuma.pet/version";
|
||||||
|
|
||||||
let interval : NodeJS.Timeout;
|
class CheckVersion {
|
||||||
|
version = packageJSON.version;
|
||||||
|
latestVersion? : string;
|
||||||
|
interval? : NodeJS.Timeout;
|
||||||
|
|
||||||
export function startInterval() {
|
async startInterval() {
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
if (await Settings.get("checkUpdate") === false) {
|
if (await Settings.get("checkUpdate") === false) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("update-checker", "Retrieving latest versions");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(CHECK_URL);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
// For debug
|
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
|
||||||
data.slow = "1000.0.0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkBeta = await Settings.get("checkBeta");
|
log.debug("update-checker", "Retrieving latest versions");
|
||||||
|
|
||||||
if (checkBeta && data.beta) {
|
try {
|
||||||
if (compareVersions.compare(data.beta, data.slow, ">")) {
|
const res = await fetch(CHECK_URL);
|
||||||
obj.latestVersion = data.beta;
|
const data = await res.json();
|
||||||
return;
|
|
||||||
|
// For debug
|
||||||
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
|
data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkBeta = await Settings.get("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && data.beta) {
|
||||||
|
if (compareVersions.compare(data.beta, data.slow, ">")) {
|
||||||
|
this.latestVersion = data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.slow) {
|
||||||
|
this.latestVersion = data.slow;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (_) {
|
||||||
|
log.info("update-checker", "Failed to check for new versions");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.slow) {
|
};
|
||||||
obj.latestVersion = data.slow;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (_) {
|
await check();
|
||||||
log.info("update-checker", "Failed to check for new versions");
|
this.interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
check();
|
|
||||||
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the check update feature
|
|
||||||
* @param value Should the check update feature be enabled?
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export async function enableCheckUpdate(value : boolean) {
|
|
||||||
await Settings.set("checkUpdate", value);
|
|
||||||
|
|
||||||
clearInterval(interval);
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
startInterval();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkVersion = new CheckVersion();
|
||||||
|
export default checkVersion;
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export class Docker {
|
|
||||||
|
|
||||||
}
|
|
@ -32,6 +32,8 @@ import User from "./models/user";
|
|||||||
import childProcessAsync from "promisify-child-process";
|
import childProcessAsync from "promisify-child-process";
|
||||||
import { Terminal } from "./terminal";
|
import { Terminal } from "./terminal";
|
||||||
|
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
export class DockgeServer {
|
export class DockgeServer {
|
||||||
app : Express;
|
app : Express;
|
||||||
httpServer : http.Server;
|
httpServer : http.Server;
|
||||||
@ -192,6 +194,39 @@ export class DockgeServer {
|
|||||||
// Create Socket.io
|
// Create Socket.io
|
||||||
this.io = new socketIO.Server(this.httpServer, {
|
this.io = new socketIO.Server(this.httpServer, {
|
||||||
cors,
|
cors,
|
||||||
|
allowRequest: (req, callback) => {
|
||||||
|
let isOriginValid = true;
|
||||||
|
const bypass = isDev;
|
||||||
|
|
||||||
|
if (!bypass) {
|
||||||
|
let host = req.headers.host;
|
||||||
|
|
||||||
|
// If this is set, it means the request is from the browser
|
||||||
|
let origin = req.headers.origin;
|
||||||
|
|
||||||
|
// If this is from the browser, check if the origin is allowed
|
||||||
|
if (origin) {
|
||||||
|
try {
|
||||||
|
let originURL = new URL(origin);
|
||||||
|
|
||||||
|
if (host !== originURL.host) {
|
||||||
|
isOriginValid = false;
|
||||||
|
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid origin url, probably not from browser
|
||||||
|
isOriginValid = false;
|
||||||
|
log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("auth", "Origin check is bypassed");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, isOriginValid);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.io.on("connection", async (socket: Socket) => {
|
this.io.on("connection", async (socket: Socket) => {
|
||||||
@ -306,6 +341,7 @@ export class DockgeServer {
|
|||||||
this.sendStackList(true);
|
this.sendStackList(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
checkVersion.startInterval();
|
||||||
});
|
});
|
||||||
|
|
||||||
gracefulShutdown(this.httpServer, {
|
gracefulShutdown(this.httpServer, {
|
||||||
@ -575,4 +611,35 @@ export class DockgeServer {
|
|||||||
finalFunction() {
|
finalFunction() {
|
||||||
log.info("server", "Graceful shutdown successful!");
|
log.info("server", "Graceful shutdown successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force connected sockets of a user to refresh and disconnect.
|
||||||
|
* Used for resetting password.
|
||||||
|
* @param {string} userID
|
||||||
|
* @param {string?} currentSocketID
|
||||||
|
*/
|
||||||
|
disconnectAllSocketClients(userID: number, currentSocketID? : string) {
|
||||||
|
for (const rawSocket of this.io.sockets.sockets.values()) {
|
||||||
|
let socket = rawSocket as DockgeSocket;
|
||||||
|
if (socket.userID === userID && socket.id !== currentSocketID) {
|
||||||
|
try {
|
||||||
|
socket.emit("refresh");
|
||||||
|
socket.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSSL() {
|
||||||
|
return this.config.sslKey && this.config.sslCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalWebSocketURL() {
|
||||||
|
const protocol = this.isSSL() ? "wss" : "ws";
|
||||||
|
const host = this.config.hostname || "localhost";
|
||||||
|
return `${protocol}://${host}:${this.config.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
|
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const stack = this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
|
const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
|
||||||
await stack.deploy(socket);
|
await stack.deploy(socket);
|
||||||
server.sendStackList();
|
server.sendStackList();
|
||||||
callback({
|
callback({
|
||||||
@ -234,7 +234,7 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
socket.on("getDockerNetworkList", async (callback) => {
|
socket.on("getDockerNetworkList", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const dockerNetworkList = server.getDockerNetworkList();
|
const dockerNetworkList = await server.getDockerNetworkList();
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
dockerNetworkList,
|
dockerNetworkList,
|
||||||
@ -264,7 +264,7 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Stack {
|
async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise<Stack> {
|
||||||
// Check types
|
// Check types
|
||||||
if (typeof(name) !== "string") {
|
if (typeof(name) !== "string") {
|
||||||
throw new ValidationError("Name must be a string");
|
throw new ValidationError("Name must be a string");
|
||||||
@ -280,7 +280,7 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stack = new Stack(server, name, composeYAML, composeENV, false);
|
const stack = new Stack(server, name, composeYAML, composeENV, false);
|
||||||
stack.save(isAdd);
|
await stack.save(isAdd);
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,6 +211,8 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
await user.resetPassword(password.newPassword);
|
await user.resetPassword(password.newPassword);
|
||||||
|
|
||||||
|
server.disconnectAllSocketClients(user.id, socket.id);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Password has been updated successfully.",
|
msg: "Password has been updated successfully.",
|
||||||
@ -280,6 +282,18 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
socket.on("disconnectOtherSocketClients", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
log.warn("disconnectOtherSocketClients", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username : string, password : string) : Promise<User | null> {
|
async login(username : string, password : string) : Promise<User | null> {
|
||||||
|
@ -162,9 +162,44 @@ export class TerminalSocketHandler extends SocketHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Resize Terminal
|
// Resize Terminal
|
||||||
socket.on("terminalResize", async (rows : unknown) => {
|
socket.on(
|
||||||
|
"terminalResize",
|
||||||
|
async (terminalName: unknown, rows: unknown, cols: unknown) => {
|
||||||
|
log.info("terminalResize", `Terminal: ${terminalName}`);
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
if (typeof terminalName !== "string") {
|
||||||
|
throw new Error("Terminal name must be a string.");
|
||||||
|
}
|
||||||
|
|
||||||
});
|
if (typeof rows !== "number") {
|
||||||
|
throw new Error("Command must be a number.");
|
||||||
|
}
|
||||||
|
if (typeof cols !== "number") {
|
||||||
|
throw new Error("Command must be a number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let terminal = Terminal.getTerminal(terminalName);
|
||||||
|
|
||||||
|
// log.info("terminal", terminal);
|
||||||
|
if (terminal instanceof Terminal) {
|
||||||
|
//log.debug("terminalInput", "Terminal found, writing to terminal.");
|
||||||
|
terminal.rows = rows;
|
||||||
|
terminal.cols = cols;
|
||||||
|
} else {
|
||||||
|
throw new Error(`${terminalName} Terminal not found.`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.debug(
|
||||||
|
"terminalResize",
|
||||||
|
// Added to prevent the lint error when adding the type
|
||||||
|
// and ts type checker saying type is unknown.
|
||||||
|
// @ts-ignore
|
||||||
|
`Error on ${terminalName}: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { DockgeServer } from "./dockge-server";
|
import { DockgeServer } from "./dockge-server";
|
||||||
import fs from "fs";
|
import fs, { promises as fsAsync } from "fs";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
import yaml from "yaml";
|
import yaml from "yaml";
|
||||||
import { DockgeSocket, ValidationError } from "./util-server";
|
import { DockgeSocket, fileExists, ValidationError } from "./util-server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
|
acceptedComposeFileNames,
|
||||||
COMBINED_TERMINAL_COLS,
|
COMBINED_TERMINAL_COLS,
|
||||||
COMBINED_TERMINAL_ROWS,
|
COMBINED_TERMINAL_ROWS,
|
||||||
CREATED_FILE,
|
CREATED_FILE,
|
||||||
@ -40,8 +41,7 @@ export class Stack {
|
|||||||
|
|
||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
// Check if compose file name is different from compose.yaml
|
// Check if compose file name is different from compose.yaml
|
||||||
const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
|
for (const filename of acceptedComposeFileNames) {
|
||||||
for (const filename of supportedFileNames) {
|
|
||||||
if (fs.existsSync(path.join(this.path, filename))) {
|
if (fs.existsSync(path.join(this.path, filename))) {
|
||||||
this._composeFileName = filename;
|
this._composeFileName = filename;
|
||||||
break;
|
break;
|
||||||
@ -99,6 +99,15 @@ export class Stack {
|
|||||||
|
|
||||||
// Check YAML format
|
// Check YAML format
|
||||||
yaml.parse(this.composeYAML);
|
yaml.parse(this.composeYAML);
|
||||||
|
|
||||||
|
let lines = this.composeENV.split("\n");
|
||||||
|
|
||||||
|
// Check if the .env is able to pass docker-compose
|
||||||
|
// Prevent "setenv: The parameter is incorrect"
|
||||||
|
// It only happens when there is one line and it doesn't contain "="
|
||||||
|
if (lines.length === 1 && !lines[0].includes("=") && lines[0].length > 0) {
|
||||||
|
throw new ValidationError("Invalid .env format");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get composeYAML() : string {
|
get composeYAML() : string {
|
||||||
@ -146,29 +155,35 @@ export class Stack {
|
|||||||
* Save the stack to the disk
|
* Save the stack to the disk
|
||||||
* @param isAdd
|
* @param isAdd
|
||||||
*/
|
*/
|
||||||
save(isAdd : boolean) {
|
async save(isAdd : boolean) {
|
||||||
this.validate();
|
this.validate();
|
||||||
|
|
||||||
let dir = this.path;
|
let dir = this.path;
|
||||||
|
|
||||||
// Check if the name is used if isAdd
|
// Check if the name is used if isAdd
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
if (fs.existsSync(dir)) {
|
if (await fileExists(dir)) {
|
||||||
throw new ValidationError("Stack name already exists");
|
throw new ValidationError("Stack name already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the stack folder
|
// Create the stack folder
|
||||||
fs.mkdirSync(dir);
|
await fsAsync.mkdir(dir);
|
||||||
} else {
|
} else {
|
||||||
if (!fs.existsSync(dir)) {
|
if (!await fileExists(dir)) {
|
||||||
throw new ValidationError("Stack not found");
|
throw new ValidationError("Stack not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write or overwrite the compose.yaml
|
// Write or overwrite the compose.yaml
|
||||||
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
|
await fsAsync.writeFile(path.join(dir, this._composeFileName), this.composeYAML);
|
||||||
|
|
||||||
|
const envPath = path.join(dir, ".env");
|
||||||
|
|
||||||
// Write or overwrite the .env
|
// Write or overwrite the .env
|
||||||
fs.writeFileSync(path.join(dir, ".env"), this.composeENV);
|
// If .env is not existing and the composeENV is empty, we don't need to write it
|
||||||
|
if (await fileExists(envPath) || this.composeENV.trim() !== "") {
|
||||||
|
await fsAsync.writeFile(envPath, this.composeENV);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
async deploy(socket? : DockgeSocket) : Promise<number> {
|
||||||
@ -188,7 +203,7 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove the stack folder
|
// Remove the stack folder
|
||||||
fs.rmSync(this.path, {
|
await fsAsync.rm(this.path, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
@ -207,6 +222,26 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a compose file exists in the specified directory.
|
||||||
|
* @async
|
||||||
|
* @static
|
||||||
|
* @param {string} stacksDir - The directory of the stack.
|
||||||
|
* @param {string} filename - The name of the directory to check for the compose file.
|
||||||
|
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether any compose file exists.
|
||||||
|
*/
|
||||||
|
static async composeFileExists(stacksDir : string, filename : string) : Promise<boolean> {
|
||||||
|
let filenamePath = path.join(stacksDir, filename);
|
||||||
|
// Check if any compose file exists
|
||||||
|
for (const filename of acceptedComposeFileNames) {
|
||||||
|
let composeFile = path.join(filenamePath, filename);
|
||||||
|
if (await fileExists(composeFile)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
|
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
|
||||||
let stacksDir = server.stacksDir;
|
let stacksDir = server.stacksDir;
|
||||||
let stackList : Map<string, Stack>;
|
let stackList : Map<string, Stack>;
|
||||||
@ -218,15 +253,19 @@ export class Stack {
|
|||||||
stackList = new Map<string, Stack>();
|
stackList = new Map<string, Stack>();
|
||||||
|
|
||||||
// Scan the stacks directory, and get the stack list
|
// Scan the stacks directory, and get the stack list
|
||||||
let filenameList = fs.readdirSync(stacksDir);
|
let filenameList = await fsAsync.readdir(stacksDir);
|
||||||
|
|
||||||
for (let filename of filenameList) {
|
for (let filename of filenameList) {
|
||||||
try {
|
try {
|
||||||
// Check if it is a directory
|
// Check if it is a directory
|
||||||
let stat = fs.statSync(path.join(stacksDir, filename));
|
let stat = await fsAsync.stat(path.join(stacksDir, filename));
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// If no compose file exists, skip it
|
||||||
|
if (!await Stack.composeFileExists(stacksDir, filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let stack = await this.getStack(server, filename);
|
let stack = await this.getStack(server, filename);
|
||||||
stack._status = CREATED_FILE;
|
stack._status = CREATED_FILE;
|
||||||
stackList.set(filename, stack);
|
stackList.set(filename, stack);
|
||||||
@ -282,7 +321,12 @@ export class Stack {
|
|||||||
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
let composeList = JSON.parse(res.toString());
|
|
||||||
|
if (!res.stdout) {
|
||||||
|
return statusList;
|
||||||
|
}
|
||||||
|
|
||||||
|
let composeList = JSON.parse(res.stdout.toString());
|
||||||
|
|
||||||
for (let composeStack of composeList) {
|
for (let composeStack of composeList) {
|
||||||
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
||||||
@ -314,7 +358,7 @@ export class Stack {
|
|||||||
let dir = path.join(server.stacksDir, stackName);
|
let dir = path.join(server.stacksDir, stackName);
|
||||||
|
|
||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) {
|
||||||
// Maybe it is a stack managed by docker compose directly
|
// Maybe it is a stack managed by docker compose directly
|
||||||
let stackList = await this.getStackList(server, true);
|
let stackList = await this.getStackList(server, true);
|
||||||
let stack = stackList.get(stackName);
|
let stack = stackList.get(stackName);
|
||||||
|
@ -67,6 +67,7 @@ export class Terminal {
|
|||||||
|
|
||||||
set cols(cols : number) {
|
set cols(cols : number) {
|
||||||
this._cols = cols;
|
this._cols = cols;
|
||||||
|
log.debug("Terminal", `Terminal cols: ${this._cols}`); // Added to check if cols is being set when changing terminal size.
|
||||||
try {
|
try {
|
||||||
this.ptyProcess?.resize(this.cols, this.rows);
|
this.ptyProcess?.resize(this.cols, this.rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
* Common utilities for backend and frontend
|
* Common utilities for backend and frontend
|
||||||
*/
|
*/
|
||||||
import { Document } from "yaml";
|
import yaml, { Document, Pair, Scalar } from "yaml";
|
||||||
|
import { DotenvParseOutput } from "dotenv";
|
||||||
|
|
||||||
// Init dayjs
|
// Init dayjs
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
// @ts-ignore
|
||||||
|
import { replaceVariablesSync } from "@inventage/envsubst";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@ -17,6 +21,11 @@ export interface LooseObject {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseRes {
|
||||||
|
ok: boolean;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
let randomBytes : (numBytes: number) => Uint8Array;
|
let randomBytes : (numBytes: number) => Uint8Array;
|
||||||
initRandomBytes();
|
initRandomBytes();
|
||||||
|
|
||||||
@ -107,6 +116,13 @@ export const allowedRawKeys = [
|
|||||||
"\u0003", // Ctrl + C
|
"\u0003", // Ctrl + C
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const acceptedComposeFileNames = [
|
||||||
|
"compose.yaml",
|
||||||
|
"docker-compose.yaml",
|
||||||
|
"docker-compose.yml",
|
||||||
|
"compose.yml",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a decimal integer number from a string
|
* Generate a decimal integer number from a string
|
||||||
* @param str Input
|
* @param str Input
|
||||||
@ -340,3 +356,52 @@ export function parseDockerPort(input : string, defaultHostname : string = "loca
|
|||||||
display: display,
|
display: display,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function envsubst(string : string, variables : LooseObject) : string {
|
||||||
|
return replaceVariablesSync(string, variables)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse all values in the yaml and for each value, if there are template variables, replace it environment variables
|
||||||
|
* Emulates the behavior of how docker-compose handles environment variables in yaml files
|
||||||
|
* @param content Yaml string
|
||||||
|
* @param env Environment variables
|
||||||
|
* @returns string Yaml string with environment variables replaced
|
||||||
|
*/
|
||||||
|
export function envsubstYAML(content : string, env : DotenvParseOutput) : string {
|
||||||
|
const doc = yaml.parseDocument(content);
|
||||||
|
if (doc.contents) {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const item of doc.contents.items) {
|
||||||
|
traverseYAML(item, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for envsubstYAML(...)
|
||||||
|
* @param pair
|
||||||
|
* @param env
|
||||||
|
*/
|
||||||
|
function traverseYAML(pair : Pair, env : DotenvParseOutput) : void {
|
||||||
|
// @ts-ignore
|
||||||
|
if (pair.value && pair.value.items) {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const item of pair.value.items) {
|
||||||
|
if (item instanceof Pair) {
|
||||||
|
traverseYAML(item, env);
|
||||||
|
} else if (item instanceof Scalar) {
|
||||||
|
let value = item.value as unknown;
|
||||||
|
|
||||||
|
if (typeof(value) === "string") {
|
||||||
|
item.value = envsubst(value, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (pair.value && typeof(pair.value.value) === "string") {
|
||||||
|
// @ts-ignore
|
||||||
|
pair.value.value = envsubst(pair.value.value, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { log } from "./log";
|
|||||||
import { ERROR_TYPE_VALIDATION } from "./util-common";
|
import { ERROR_TYPE_VALIDATION } from "./util-common";
|
||||||
import { R } from "redbean-node";
|
import { R } from "redbean-node";
|
||||||
import { verifyPassword } from "./password-hash";
|
import { verifyPassword } from "./password-hash";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
export interface JWTDecoded {
|
export interface JWTDecoded {
|
||||||
username : string;
|
username : string;
|
||||||
@ -82,3 +83,9 @@ export async function doubleCheckPassword(socket : DockgeSocket, currentPassword
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fileExists(file : string) {
|
||||||
|
return fs.promises.access(file, fs.constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
FROM node:18.17.1-bookworm-slim
|
FROM node:18.17.1-bookworm-slim
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
# TARGETPLATFORM: linux/amd64, linux/arm64, linux/arm/v7
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# TARGETARCH: amd64, arm64, arm/v7
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
RUN apt update && apt install --yes --no-install-recommends \
|
RUN apt update && apt install --yes --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -18,7 +26,12 @@ RUN apt update && apt install --yes --no-install-recommends \
|
|||||||
&& apt update \
|
&& apt update \
|
||||||
&& apt --yes --no-install-recommends install \
|
&& apt --yes --no-install-recommends install \
|
||||||
docker-ce-cli \
|
docker-ce-cli \
|
||||||
docker-compose-plugin \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& npm install pnpm -g \
|
&& npm install pnpm -g \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
|
# Download docker-compose, as the repo's docker-compose is not up-to-date.
|
||||||
|
COPY ./extra/download-docker-compose.ts ./extra/download-docker-compose.ts
|
||||||
|
ARG DOCKER_COMPOSE_VERSION="2.23.3"
|
||||||
|
RUN tsx ./extra/download-docker-compose.ts ${TARGETPLATFORM} ${DOCKER_COMPOSE_VERSION} \
|
||||||
|
&& docker compose version
|
||||||
|
39
extra/download-docker-compose.ts
Normal file
39
extra/download-docker-compose.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// TARGETPLATFORM
|
||||||
|
const targetPlatform = process.argv[2];
|
||||||
|
|
||||||
|
// Docker Compose version
|
||||||
|
const dockerComposeVersion = process.argv[3];
|
||||||
|
|
||||||
|
// Arch
|
||||||
|
let arch = "";
|
||||||
|
|
||||||
|
if (targetPlatform === "linux/amd64") {
|
||||||
|
arch = "x86_64";
|
||||||
|
} else if (targetPlatform === "linux/arm64") {
|
||||||
|
arch = "aarch64";
|
||||||
|
} else if (targetPlatform === "linux/arm/v7") {
|
||||||
|
arch = "armv7";
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown target platform: ${targetPlatform}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mkdir -p /root/.docker/cli-plugins
|
||||||
|
fs.mkdirSync("/root/.docker/cli-plugins", { recursive: true });
|
||||||
|
|
||||||
|
// Download URL
|
||||||
|
const url = `https://github.com/docker/compose/releases/download/v${dockerComposeVersion}/docker-compose-linux-${arch}`;
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
// Download docker-compose using fetch api, to "/root/.docker/cli-plugins/docker-compose"
|
||||||
|
const buffer = await (await fetch(url)).arrayBuffer();
|
||||||
|
fs.writeFileSync("/root/.docker/cli-plugins/docker-compose", Buffer.from(buffer));
|
||||||
|
|
||||||
|
// chmod +x /root/.docker/cli-plugins/docker-compose
|
||||||
|
fs.chmodSync("/root/.docker/cli-plugins/docker-compose", 0o111);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
@ -4,16 +4,30 @@ const input = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
|
||||||
### 🆕 New Features
|
### 🆕 New Features
|
||||||
|
-
|
||||||
|
|
||||||
### Improvements
|
### ⬆️ Improvements
|
||||||
|
-
|
||||||
|
|
||||||
### 🐞 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
-
|
||||||
|
|
||||||
### 🦎 Translation Contributions
|
### 🦎 Translation Contributions
|
||||||
|
-
|
||||||
|
|
||||||
|
### ⬆️ Security Fixes
|
||||||
|
-
|
||||||
|
|
||||||
### Others
|
### Others
|
||||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
|
-
|
||||||
|
|
||||||
|
Please let me know if your username is missing, if your pull request has been merged in this version, or your commit has been included in one of the pull requests.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||||
@ -37,6 +51,12 @@ for (const line of lines) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = message.split("* ").pop();
|
message = message.split("* ").pop();
|
||||||
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
|
||||||
|
let thanks = "";
|
||||||
|
if (username != "@louislam") {
|
||||||
|
thanks = `(Thanks ${username})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(pullRequestID, message, thanks);
|
||||||
}
|
}
|
||||||
console.log(template);
|
console.log(template);
|
||||||
|
@ -4,6 +4,8 @@ import readline from "readline";
|
|||||||
import { User } from "../backend/models/user";
|
import { User } from "../backend/models/user";
|
||||||
import { DockgeServer } from "../backend/dockge-server";
|
import { DockgeServer } from "../backend/dockge-server";
|
||||||
import { log } from "../backend/log";
|
import { log } from "../backend/log";
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { BaseRes } from "../backend/util-common";
|
||||||
|
|
||||||
console.log("== Dockge Reset Password Tool ==");
|
console.log("== Dockge Reset Password Tool ==");
|
||||||
|
|
||||||
@ -12,11 +14,10 @@ const rl = readline.createInterface({
|
|||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const server = new DockgeServer();
|
||||||
|
|
||||||
export const main = async () => {
|
export const main = async () => {
|
||||||
const server = new DockgeServer();
|
|
||||||
|
|
||||||
// Check if
|
// Check if
|
||||||
|
|
||||||
console.log("Connecting the database");
|
console.log("Connecting the database");
|
||||||
try {
|
try {
|
||||||
await Database.init(server);
|
await Database.init(server);
|
||||||
@ -47,12 +48,16 @@ export const main = async () => {
|
|||||||
// Reset all sessions by reset jwt secret
|
// Reset all sessions by reset jwt secret
|
||||||
await server.initJWTSecret();
|
await server.initJWTSecret();
|
||||||
|
|
||||||
|
console.log("Password reset successfully.");
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
await disconnectAllSocketClients(user.username, password);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
console.log("Passwords do not match, please try again.");
|
console.log("Passwords do not match, please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Password reset successfully.");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@ -79,6 +84,47 @@ function question(question : string) : Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disconnectAllSocketClients(username : string, password : string) : Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const url = server.getLocalWebSocketURL();
|
||||||
|
|
||||||
|
console.log("Connecting to " + url + " to disconnect all other socket clients");
|
||||||
|
|
||||||
|
// Disconnect all socket connections
|
||||||
|
const socket = io(url, {
|
||||||
|
transports: [ "websocket" ],
|
||||||
|
reconnection: false,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit("login", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}, (res : BaseRes) => {
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("Logged in.");
|
||||||
|
socket.emit("disconnectOtherSocketClients");
|
||||||
|
} else {
|
||||||
|
console.warn("Login failed.");
|
||||||
|
console.warn("Please restart the server to disconnect all sessions.");
|
||||||
|
}
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", function () {
|
||||||
|
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
|
||||||
|
// Ask the user to restart the server manually
|
||||||
|
console.warn("Failed to connect to " + url);
|
||||||
|
console.warn("Please restart the server to disconnect all sessions manually.");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.TEST_BACKEND) {
|
if (!process.env.TEST_BACKEND) {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div v-if="!isEditMode">
|
<div v-if="!isEditMode">
|
||||||
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
|
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
|
||||||
|
|
||||||
<a v-for="port in service.ports" :key="port" :href="parsePort(port).url" target="_blank">
|
<a v-for="port in envsubstService.ports" :key="port" :href="parsePort(port).url" target="_blank">
|
||||||
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
|
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -213,16 +213,29 @@ export default defineComponent({
|
|||||||
jsonObject() {
|
jsonObject() {
|
||||||
return this.$parent.$parent.jsonConfig;
|
return this.$parent.$parent.jsonConfig;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
envsubstJSONConfig() {
|
||||||
|
return this.$parent.$parent.envsubstJSONConfig;
|
||||||
|
},
|
||||||
|
|
||||||
|
envsubstService() {
|
||||||
|
if (!this.envsubstJSONConfig.services[this.name]) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return this.envsubstJSONConfig.services[this.name];
|
||||||
|
},
|
||||||
|
|
||||||
imageName() {
|
imageName() {
|
||||||
if (this.service.image) {
|
if (this.envsubstService.image) {
|
||||||
return this.service.image.split(":")[0];
|
return this.envsubstService.image.split(":")[0];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
imageTag() {
|
imageTag() {
|
||||||
if (this.service.image) {
|
if (this.envsubstService.image) {
|
||||||
let tag = this.service.image.split(":")[1];
|
let tag = this.envsubstService.image.split(":")[1];
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return tag;
|
return tag;
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Terminal } from "xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||||
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
|
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
|
||||||
|
|
||||||
@ -122,10 +123,12 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Fit the terminal width to the div container size after terminal is created.
|
||||||
|
this.updateTerminalSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
unmounted() {
|
||||||
|
window.removeEventListener("resize", this.onResizeEvent); // Remove the resize event listener from the window object.
|
||||||
this.$root.unbindTerminal(this.name);
|
this.$root.unbindTerminal(this.name);
|
||||||
this.terminal.dispose();
|
this.terminal.dispose();
|
||||||
},
|
},
|
||||||
@ -208,6 +211,30 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the terminal size to fit the container size.
|
||||||
|
*
|
||||||
|
* If the terminalFitAddOn is not created, creates it, loads it and then fits the terminal to the appropriate size.
|
||||||
|
* It then addes an event listener to the window object to listen for resize events and calls the fit method of the terminalFitAddOn.
|
||||||
|
*/
|
||||||
|
updateTerminalSize() {
|
||||||
|
if (!Object.hasOwn(this, "terminalFitAddOn")) {
|
||||||
|
this.terminalFitAddOn = new FitAddon();
|
||||||
|
this.terminal.loadAddon(this.terminalFitAddOn);
|
||||||
|
window.addEventListener("resize", this.onResizeEvent);
|
||||||
|
}
|
||||||
|
this.terminalFitAddOn.fit();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handles the resize event of the terminal component.
|
||||||
|
*/
|
||||||
|
onResizeEvent() {
|
||||||
|
this.terminalFitAddOn.fit();
|
||||||
|
let rows = this.terminal.rows;
|
||||||
|
let cols = this.terminal.cols;
|
||||||
|
this.$root.getSocket().emit("terminalResize", this.name, rows, cols);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,7 @@ for (let lang in languageList) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rtlLangs = [ "fa", "ar-SY", "ur" ];
|
const rtlLangs = [ "fa", "ar-SY", "ur", "ar" ];
|
||||||
|
|
||||||
export const currentLocale = () => localStorage.locale
|
export const currentLocale = () => localStorage.locale
|
||||||
|| languageList[navigator.language] && navigator.language
|
|| languageList[navigator.language] && navigator.language
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"home": "Startseite",
|
"home": "Startseite",
|
||||||
"console": "Konsole",
|
"console": "Konsole",
|
||||||
"registry": "Container Registry",
|
"registry": "Container Registry",
|
||||||
"compose": "",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
|
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
|
||||||
"stackName": "Stack-Name",
|
"stackName": "Stack-Name",
|
||||||
"deployStack": "Deployen",
|
"deployStack": "Deployen",
|
||||||
@ -72,15 +72,15 @@
|
|||||||
"Check Update On GitHub": "Update auf GitHub überprüfen",
|
"Check Update On GitHub": "Update auf GitHub überprüfen",
|
||||||
"Show update if available": "Update anzeigen, wenn verfügbar",
|
"Show update if available": "Update anzeigen, wenn verfügbar",
|
||||||
"Also check beta release": "Auch Beta-Version überprüfen",
|
"Also check beta release": "Auch Beta-Version überprüfen",
|
||||||
"Remember me": "Anmeldung beibehalten",
|
"Remember me": "Angemeldet bleiben",
|
||||||
"Login": "Anmelden",
|
"Login": "Anmelden",
|
||||||
"Username": "Benutzername",
|
"Username": "Benutzername",
|
||||||
"Password": "Passwort",
|
"Password": "Passwort",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
"Logout": "Abmelden",
|
"Logout": "Abmelden",
|
||||||
"Lowercase only": "Nur Kleinbuchstaben",
|
"Lowercase only": "Nur Kleinbuchstaben",
|
||||||
"Convert to Compose": "In Compose Syntax umwandeln",
|
"Convert to Compose": "In Compose-Syntax umwandeln",
|
||||||
"Docker Run": "Docker ausführen",
|
"Docker Run": "Docker Run",
|
||||||
"active": "aktiv",
|
"active": "aktiv",
|
||||||
"exited": "beendet",
|
"exited": "beendet",
|
||||||
"inactive": "inaktiv",
|
"inactive": "inaktiv",
|
||||||
|
@ -98,5 +98,6 @@
|
|||||||
"reconnecting...": "Reconnecting…",
|
"reconnecting...": "Reconnecting…",
|
||||||
"connecting...": "Connecting to the socket server…",
|
"connecting...": "Connecting to the socket server…",
|
||||||
"url": "URL | URLs",
|
"url": "URL | URLs",
|
||||||
"extra": "Extra"
|
"extra": "Extra",
|
||||||
|
"newUpdate": "New Update"
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"registry": "Registro",
|
"registry": "Registro",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Componi il tuo primo stack!",
|
"addFirstStackMsg": "Componi il tuo primo stack!",
|
||||||
"stackName" : "Nome dello stack",
|
"stackName": "Nome dello stack",
|
||||||
"deployStack": "Deploy",
|
"deployStack": "Deploy",
|
||||||
"deleteStack": "Cancella",
|
"deleteStack": "Cancella",
|
||||||
"stopStack": "Stop",
|
"stopStack": "Stop",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"editStack": "Modifica",
|
"editStack": "Modifica",
|
||||||
"discardStack": "Annulla",
|
"discardStack": "Annulla",
|
||||||
"saveStackDraft": "Salva",
|
"saveStackDraft": "Salva",
|
||||||
"notAvailableShort" : "N/D",
|
"notAvailableShort": "N/D",
|
||||||
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
|
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
|
||||||
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
|
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
|
||||||
"primaryHostname": "Hostname primario",
|
"primaryHostname": "Hostname primario",
|
||||||
@ -91,5 +91,11 @@
|
|||||||
"Allowed commands:": "Comandi permessi:",
|
"Allowed commands:": "Comandi permessi:",
|
||||||
"Internal Networks": "Reti interne",
|
"Internal Networks": "Reti interne",
|
||||||
"External Networks": "Reti esterne",
|
"External Networks": "Reti esterne",
|
||||||
"No External Networks": "Nessuna rete esterna"
|
"No External Networks": "Nessuna rete esterna",
|
||||||
|
"reverseProxyMsg1": "Utilizzando un proxy inverso?",
|
||||||
|
"reverseProxyMsg2": "Controlla come configurarlo per WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Impossibile connettersi al server socket.",
|
||||||
|
"connecting...": "Connessione al server socket…",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Riconnessione…"
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"editStack": "編集",
|
"editStack": "編集",
|
||||||
"discardStack": "破棄",
|
"discardStack": "破棄",
|
||||||
"saveStackDraft": "保存",
|
"saveStackDraft": "保存",
|
||||||
"stackNotManagedByDockgeMsg": "このスタックはDockageによって管理されていません。",
|
"stackNotManagedByDockgeMsg": "このスタックはDockgeによって管理されていません。",
|
||||||
"general": "一般",
|
"general": "一般",
|
||||||
"scanFolder": "スタックフォルダをスキャン",
|
"scanFolder": "スタックフォルダをスキャン",
|
||||||
"dockerImage": "イメージ",
|
"dockerImage": "イメージ",
|
||||||
|
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "허용된 명령어:",
|
"Allowed commands:": "허용된 명령어:",
|
||||||
"Internal Networks": "내부 네트워크",
|
"Internal Networks": "내부 네트워크",
|
||||||
"External Networks": "외부 네트워크",
|
"External Networks": "외부 네트워크",
|
||||||
"No External Networks": "외부 네트워크 없음"
|
"No External Networks": "외부 네트워크 없음",
|
||||||
|
"reverseProxyMsg2": "여기서 WebSocket을 위한 설정을 확인해 보세요",
|
||||||
|
"downStack": "정지 & Down",
|
||||||
|
"reverseProxyMsg1": "리버스 프록시를 사용하고 계신가요?",
|
||||||
|
"Cannot connect to the socket server.": "소켓 서버에 연결하지 못했습니다.",
|
||||||
|
"connecting...": "소켓 서버에 연결하는 중…",
|
||||||
|
"extra": "기타",
|
||||||
|
"url": "URL | URL",
|
||||||
|
"reconnecting...": "재연결 중…"
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"registry": "Registro",
|
"registry": "Registro",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Crie sua primeira stack!",
|
"addFirstStackMsg": "Crie sua primeira stack!",
|
||||||
"stackName" : "Nome da stack",
|
"stackName": "Nome da stack",
|
||||||
"deployStack": "Deploy",
|
"deployStack": "Deploy",
|
||||||
"deleteStack": "Excluir",
|
"deleteStack": "Excluir",
|
||||||
"stopStack": "Parar",
|
"stopStack": "Parar",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"editStack": "Editar",
|
"editStack": "Editar",
|
||||||
"discardStack": "Descartar",
|
"discardStack": "Descartar",
|
||||||
"saveStackDraft": "Salvar",
|
"saveStackDraft": "Salvar",
|
||||||
"notAvailableShort" : "N/D",
|
"notAvailableShort": "N/D",
|
||||||
"deleteStackMsg": "Tem certeza que deseja excluir esta stack?",
|
"deleteStackMsg": "Tem certeza que deseja excluir esta stack?",
|
||||||
"stackNotManagedByDockgeMsg": "Esta stack não é gerenciada pelo Dockge.",
|
"stackNotManagedByDockgeMsg": "Esta stack não é gerenciada pelo Dockge.",
|
||||||
"primaryHostname": "Nome do Host Primário",
|
"primaryHostname": "Nome do Host Primário",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Comandos permitidos:",
|
"Allowed commands:": "Comandos permitidos:",
|
||||||
"Internal Networks": "Redes internas",
|
"Internal Networks": "Redes internas",
|
||||||
"External Networks": "Redes externas",
|
"External Networks": "Redes externas",
|
||||||
"No External Networks": "Sem redes externas"
|
"No External Networks": "Sem redes externas",
|
||||||
|
"reverseProxyMsg2": "Veja como configurar para WebSocket",
|
||||||
|
"downStack": "Parar & Encerrar",
|
||||||
|
"reverseProxyMsg1": "Utiliza proxy reverso?",
|
||||||
|
"Cannot connect to the socket server.": "Não é possível conectar ao socket server.",
|
||||||
|
"connecting...": "Conectando ao socket server…",
|
||||||
|
"url": "URL | URLs",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Reconectando…"
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
"PasswordsDoNotMatch": "Пароль не совпадает.",
|
"PasswordsDoNotMatch": "Пароль не совпадает.",
|
||||||
"Repeat Password": "Повторите пароль",
|
"Repeat Password": "Повторите пароль",
|
||||||
"Create": "Создать",
|
"Create": "Создать",
|
||||||
"signedInDisp": "Авторизован как",
|
"signedInDisp": "Авторизован как {0}",
|
||||||
"signedInDispDisabled": "Авторизация выключена.",
|
"signedInDispDisabled": "Авторизация выключена.",
|
||||||
"home": "Главная",
|
"home": "Главная",
|
||||||
"console": "Консоль",
|
"console": "Консоль",
|
||||||
"registry": "Registry",
|
"registry": "Реестр (Registry)",
|
||||||
"compose": "Compose",
|
"compose": "Составить (Compose)",
|
||||||
"addFirstStackMsg": "Создайте свой первый стек!",
|
"addFirstStackMsg": "Создайте свой первый стек!",
|
||||||
"stackName": "Имя стека",
|
"stackName": "Имя стека",
|
||||||
"deployStack": "Развернуть",
|
"deployStack": "Развернуть",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Разрешенные команды:",
|
"Allowed commands:": "Разрешенные команды:",
|
||||||
"Internal Networks": "Внутренние сети",
|
"Internal Networks": "Внутренние сети",
|
||||||
"External Networks": "Внешние сети",
|
"External Networks": "Внешние сети",
|
||||||
"No External Networks": "Нет внешних сетей"
|
"No External Networks": "Нет внешних сетей",
|
||||||
|
"downStack": "Остановить и выключить",
|
||||||
|
"reverseProxyMsg1": "Использовать Реверс Прокси?",
|
||||||
|
"reconnecting...": "Переподключение…",
|
||||||
|
"Cannot connect to the socket server.": "Не удается подключиться к серверу сокетов.",
|
||||||
|
"url": "URL адрес(а)",
|
||||||
|
"extra": "Дополнительно",
|
||||||
|
"reverseProxyMsg2": "Проверьте, как настроить его для WebSocket",
|
||||||
|
"connecting...": "Подключение к серверу сокетов…"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"languageName": "Svenska",
|
"languageName": "Svenska",
|
||||||
"Create your admin account": "Skapa ditt Admin-konto.",
|
"Create your admin account": "Skapa ditt Admin-konto",
|
||||||
"authIncorrectCreds": "Fel användarnamn eller lösenord.",
|
"authIncorrectCreds": "Fel användarnamn eller lösenord.",
|
||||||
"PasswordsDoNotMatch": "Lösenorden matchar inte.",
|
"PasswordsDoNotMatch": "Lösenorden matchar inte.",
|
||||||
"Repeat Password": "Repetera lösenord",
|
"Repeat Password": "Repetera lösenord",
|
||||||
@ -12,28 +12,28 @@
|
|||||||
"registry": "Register",
|
"registry": "Register",
|
||||||
"compose": "Komponera",
|
"compose": "Komponera",
|
||||||
"addFirstStackMsg": "Komponera din första stack!",
|
"addFirstStackMsg": "Komponera din första stack!",
|
||||||
"stackName" : "Stacknamn",
|
"stackName": "Stacknamn",
|
||||||
"deployStack": "Distribuera",
|
"deployStack": "Distribuera",
|
||||||
"deleteStack": "Radera",
|
"deleteStack": "Radera",
|
||||||
"stopStack": "Stop",
|
"stopStack": "Stoppa",
|
||||||
"restartStack": "Starta om",
|
"restartStack": "Starta om",
|
||||||
"updateStack": "Uppdatera",
|
"updateStack": "Uppdatera",
|
||||||
"startStack": "Starta",
|
"startStack": "Starta",
|
||||||
"downStack": "Stop & Ner",
|
"downStack": "Stoppa & Ner",
|
||||||
"editStack": "Redigera",
|
"editStack": "Redigera",
|
||||||
"discardStack": "Kasta",
|
"discardStack": "Kasta",
|
||||||
"saveStackDraft": "Spara",
|
"saveStackDraft": "Spara",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/A",
|
||||||
"deleteStackMsg": "Är du säker på att du vill radera stacken?",
|
"deleteStackMsg": "Är du säker på att du vill radera stacken?",
|
||||||
"stackNotManagedByDockgeMsg": "Denna stacken hanteras inte av Dockge.",
|
"stackNotManagedByDockgeMsg": "Denna stacken hanteras inte av Dockge.",
|
||||||
"primaryHostname": "Primärt värdnamn",
|
"primaryHostname": "Primärt värdnamn",
|
||||||
"general": "Allmän",
|
"general": "Allmän",
|
||||||
"container": "Container | Containrar",
|
"container": "Container | Containrar",
|
||||||
"scanFolder": "Scanna Stackfolder",
|
"scanFolder": "Skanna Stackmapp",
|
||||||
"dockerImage": "Bild",
|
"dockerImage": "Avbild",
|
||||||
"restartPolicyUnlessStopped": "Om inte stoppas",
|
"restartPolicyUnlessStopped": "Om inte stoppad",
|
||||||
"restartPolicyAlways": "Alltid",
|
"restartPolicyAlways": "Alltid",
|
||||||
"restartPolicyOnFailure": "Vid Misslyckande",
|
"restartPolicyOnFailure": "Vid misslyckande",
|
||||||
"restartPolicyNo": "Nej",
|
"restartPolicyNo": "Nej",
|
||||||
"environmentVariable": "Miljövariabel | Miljövariabler",
|
"environmentVariable": "Miljövariabel | Miljövariabler",
|
||||||
"restartPolicy": "Omstartspolicy",
|
"restartPolicy": "Omstartspolicy",
|
||||||
@ -44,12 +44,12 @@
|
|||||||
"dependsOn": "Containerberoende | Containerberoenden",
|
"dependsOn": "Containerberoende | Containerberoenden",
|
||||||
"addListItem": "Lägg till {0}",
|
"addListItem": "Lägg till {0}",
|
||||||
"deleteContainer": "Radera",
|
"deleteContainer": "Radera",
|
||||||
"addContainer": "Lägg till Container",
|
"addContainer": "Lägg till container",
|
||||||
"addNetwork": "Lägg till Nätverk",
|
"addNetwork": "Lägg till nätverk",
|
||||||
"disableauth.message1": "Är du säker på att du vill <strong>inaktivera autentisering</strong>?",
|
"disableauth.message1": "Är du säker på att du vill <strong>inaktivera autentisering</strong>?",
|
||||||
"disableauth.message2": "Det är designat för senarion <stong>när du ska implementera tredjeparts autentisering</strong> framör Dockge som Cloudflare Access, Authelia eller andra autentiseringsmekanismer.",
|
"disableauth.message2": "Det är designat för scenarion <strong>där du ska implementera tredjepartsautentisering</strong> framför Dockge som Cloudflare Access, Authelia eller andra autentiseringsmekanismer.",
|
||||||
"passwordNotMatchMsg": "Det upprepade lösenordet matchar inte",
|
"passwordNotMatchMsg": "Det upprepade lösenordet matchar inte.",
|
||||||
"autoGet": "Auto Hämta",
|
"autoGet": "Auto-hämta",
|
||||||
"add": "Lägg till",
|
"add": "Lägg till",
|
||||||
"Edit": "Redigera",
|
"Edit": "Redigera",
|
||||||
"applyToYAML": "Lägg till i YAML",
|
"applyToYAML": "Lägg till i YAML",
|
||||||
@ -57,8 +57,8 @@
|
|||||||
"addInternalNetwork": "Lägg till",
|
"addInternalNetwork": "Lägg till",
|
||||||
"Save": "Spara",
|
"Save": "Spara",
|
||||||
"Language": "Språk",
|
"Language": "Språk",
|
||||||
"Current User": "Nuvarande användaren",
|
"Current User": "Nuvarande användare",
|
||||||
"Change Password": "Byt lösenord",
|
"Change Password": "Ändra lösenord",
|
||||||
"Current Password": "Nuvarande lösenord",
|
"Current Password": "Nuvarande lösenord",
|
||||||
"New Password": "Nytt lösenord",
|
"New Password": "Nytt lösenord",
|
||||||
"Repeat New Password": "Upprepa nytt lösenord",
|
"Repeat New Password": "Upprepa nytt lösenord",
|
||||||
@ -70,9 +70,9 @@
|
|||||||
"I understand, please disable": "Jag förstår, vänligen inaktivera",
|
"I understand, please disable": "Jag förstår, vänligen inaktivera",
|
||||||
"Leave": "Lämna",
|
"Leave": "Lämna",
|
||||||
"Frontend Version": "Frontendversion",
|
"Frontend Version": "Frontendversion",
|
||||||
"Check Update On GitHub": "Kontrollera Uppdatering på GitHub",
|
"Check Update On GitHub": "Kontrollera uppdatering på GitHub",
|
||||||
"Show update if available": "Visa uppdatering om tillgänglig",
|
"Show update if available": "Visa uppdatering om tillgänglig",
|
||||||
"Also check beta release": "Kontrollera även betaversionen",
|
"Also check beta release": "Kontrollera även betaversioner",
|
||||||
"Remember me": "Kom ihåg mig",
|
"Remember me": "Kom ihåg mig",
|
||||||
"Login": "Logga in",
|
"Login": "Logga in",
|
||||||
"Username": "Användarnamn",
|
"Username": "Användarnamn",
|
||||||
@ -80,8 +80,8 @@
|
|||||||
"Settings": "Inställningar",
|
"Settings": "Inställningar",
|
||||||
"Logout": "Logga ut",
|
"Logout": "Logga ut",
|
||||||
"Lowercase only": "Endast små tecken",
|
"Lowercase only": "Endast små tecken",
|
||||||
"Convert to Compose": "Omvandla till Compose",
|
"Convert to Compose": "Omvandla till compose",
|
||||||
"Docker Run": "Docker Run",
|
"Docker Run": "Docker kör",
|
||||||
"active": "aktiv",
|
"active": "aktiv",
|
||||||
"exited": "avslutad",
|
"exited": "avslutad",
|
||||||
"inactive": "inaktiv",
|
"inactive": "inaktiv",
|
||||||
@ -89,7 +89,14 @@
|
|||||||
"Security": "Säkerhet",
|
"Security": "Säkerhet",
|
||||||
"About": "Om",
|
"About": "Om",
|
||||||
"Allowed commands:": "Tillåtna kommandon:",
|
"Allowed commands:": "Tillåtna kommandon:",
|
||||||
"Internal Networks": "Interna Nätverk",
|
"Internal Networks": "Interna nätverk",
|
||||||
"External Networks": "Externa Nätverk",
|
"External Networks": "Externa nätverk",
|
||||||
"No External Networks": "Inga Externa Nätverk"
|
"No External Networks": "Inga externa nätverk",
|
||||||
|
"reverseProxyMsg1": "Används omvänd proxy?",
|
||||||
|
"connecting...": "Ansluter till socketserver…",
|
||||||
|
"Cannot connect to the socket server.": "Kan inte ansluta till socketservern.",
|
||||||
|
"reverseProxyMsg2": "Kontrollera hur man konfigurerar webbsocket",
|
||||||
|
"url": "URL | URLer",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Återansluter…"
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
"External Networks": "外部網路",
|
"External Networks": "外部網路",
|
||||||
"No External Networks": "無外部網路",
|
"No External Networks": "無外部網路",
|
||||||
"downStack": "停止",
|
"downStack": "停止",
|
||||||
"reverseProxyMsg1": "在使用反向代理吗?",
|
"reverseProxyMsg1": "在使用反向代理嗎?",
|
||||||
"reverseProxyMsg2": "點擊這裡了解如何為 WebSocket 配置反向代理",
|
"reverseProxyMsg2": "點擊這裡了解如何為 WebSocket 配置反向代理",
|
||||||
"Cannot connect to the socket server.": "無法連接到 Socket 伺服器。",
|
"Cannot connect to the socket server.": "無法連接到 Socket 伺服器。",
|
||||||
"reconnecting...": "重新連線中…",
|
"reconnecting...": "重新連線中…",
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
<span class="fs-4 title">Dockge</span>
|
<span class="fs-4 title">Dockge</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-info me-3">
|
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-warning me-3">
|
||||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
|
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("newUpdate") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
|
@ -10,12 +10,12 @@ import { i18n } from "./i18n";
|
|||||||
// Dependencies
|
// Dependencies
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import Toast, { POSITION, useToast } from "vue-toastification";
|
import Toast, { POSITION, useToast } from "vue-toastification";
|
||||||
import "xterm/lib/xterm.js";
|
import "@xterm/xterm/lib/xterm.js";
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
import "@fontsource/jetbrains-mono";
|
import "@fontsource/jetbrains-mono";
|
||||||
import "vue-toastification/dist/index.css";
|
import "vue-toastification/dist/index.css";
|
||||||
import "xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
|
|
||||||
// Minxins
|
// Minxins
|
||||||
|
@ -2,7 +2,7 @@ import { io } from "socket.io-client";
|
|||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { Terminal } from "xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
|
||||||
let socket : Socket;
|
let socket : Socket;
|
||||||
|
|
||||||
@ -202,6 +202,10 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("refresh", () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -231,7 +231,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|||||||
import {
|
import {
|
||||||
COMBINED_TERMINAL_COLS,
|
COMBINED_TERMINAL_COLS,
|
||||||
COMBINED_TERMINAL_ROWS,
|
COMBINED_TERMINAL_ROWS,
|
||||||
copyYAMLComments,
|
copyYAMLComments, envsubstYAML,
|
||||||
getCombinedTerminalName,
|
getCombinedTerminalName,
|
||||||
getComposeTerminalName,
|
getComposeTerminalName,
|
||||||
PROGRESS_TERMINAL_ROWS,
|
PROGRESS_TERMINAL_ROWS,
|
||||||
@ -239,6 +239,7 @@ import {
|
|||||||
} from "../../../backend/util-common";
|
} from "../../../backend/util-common";
|
||||||
import { BModal } from "bootstrap-vue-next";
|
import { BModal } from "bootstrap-vue-next";
|
||||||
import NetworkInput from "../components/NetworkInput.vue";
|
import NetworkInput from "../components/NetworkInput.vue";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
const template = `version: "3.8"
|
const template = `version: "3.8"
|
||||||
services:
|
services:
|
||||||
@ -277,6 +278,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
editorFocus: false,
|
editorFocus: false,
|
||||||
jsonConfig: {},
|
jsonConfig: {},
|
||||||
|
envsubstJSONConfig: {},
|
||||||
yamlError: "",
|
yamlError: "",
|
||||||
processing: true,
|
processing: true,
|
||||||
showProgressTerminal: false,
|
showProgressTerminal: false,
|
||||||
@ -297,12 +299,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
urls() {
|
urls() {
|
||||||
if (!this.jsonConfig["x-dockge"] || !this.jsonConfig["x-dockge"].urls || !Array.isArray(this.jsonConfig["x-dockge"].urls)) {
|
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let urls = [];
|
let urls = [];
|
||||||
for (const url of this.jsonConfig["x-dockge"].urls) {
|
for (const url of this.envsubstJSONConfig["x-dockge"].urls) {
|
||||||
let display;
|
let display;
|
||||||
try {
|
try {
|
||||||
let obj = new URL(url);
|
let obj = new URL(url);
|
||||||
@ -372,6 +374,17 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"stack.composeENV": {
|
||||||
|
handler() {
|
||||||
|
if (this.editorFocus) {
|
||||||
|
console.debug("env code changed");
|
||||||
|
this.yamlCodeChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
|
||||||
jsonConfig: {
|
jsonConfig: {
|
||||||
handler() {
|
handler() {
|
||||||
if (!this.editorFocus) {
|
if (!this.editorFocus) {
|
||||||
@ -622,7 +635,7 @@ export default {
|
|||||||
greedy: true
|
greedy: true
|
||||||
},
|
},
|
||||||
"keyword": {
|
"keyword": {
|
||||||
pattern: /^[^ :=]*(?=[:=])/m,
|
pattern: /^\w*(?=[:=])/m,
|
||||||
greedy: true
|
greedy: true
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
@ -645,28 +658,41 @@ export default {
|
|||||||
return highlight(code, languages.docker_env);
|
return highlight(code, languages.docker_env);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
yamlToJSON(yaml) {
|
||||||
|
let doc = parseDocument(yaml);
|
||||||
|
if (doc.errors.length > 0) {
|
||||||
|
throw doc.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = doc.toJS() ?? {};
|
||||||
|
|
||||||
|
// Check data types
|
||||||
|
// "services" must be an object
|
||||||
|
if (!config.services) {
|
||||||
|
config.services = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
||||||
|
throw new Error("Services must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
doc,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
yamlCodeChange() {
|
yamlCodeChange() {
|
||||||
try {
|
try {
|
||||||
let doc = parseDocument(this.stack.composeYAML);
|
let { config, doc } = this.yamlToJSON(this.stack.composeYAML);
|
||||||
if (doc.errors.length > 0) {
|
|
||||||
throw doc.errors[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = doc.toJS() ?? {};
|
|
||||||
|
|
||||||
// Check data types
|
|
||||||
// "services" must be an object
|
|
||||||
if (!config.services) {
|
|
||||||
config.services = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
|
||||||
throw new Error("Services must be an object");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.yamlDoc = doc;
|
this.yamlDoc = doc;
|
||||||
this.jsonConfig = config;
|
this.jsonConfig = config;
|
||||||
|
|
||||||
|
let env = dotenv.parse(this.stack.composeENV);
|
||||||
|
let envYAML = envsubstYAML(this.stack.composeYAML, env);
|
||||||
|
this.envsubstJSONConfig = this.yamlToJSON(envYAML).config;
|
||||||
|
|
||||||
clearTimeout(yamlErrorTimeout);
|
clearTimeout(yamlErrorTimeout);
|
||||||
this.yamlError = "";
|
this.yamlError = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dockge",
|
"name": "dockge",
|
||||||
"version": "1.2.0",
|
"version": "1.3.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18.0.0 && <= 18.17.1"
|
"node": ">= 18.0.0 && <= 18.17.1"
|
||||||
@ -10,6 +10,7 @@
|
|||||||
"lint": "eslint \"**/*.{ts,vue}\"",
|
"lint": "eslint \"**/*.{ts,vue}\"",
|
||||||
"check-ts": "tsc --noEmit",
|
"check-ts": "tsc --noEmit",
|
||||||
"start": "tsx ./backend/index.ts",
|
"start": "tsx ./backend/index.ts",
|
||||||
|
"dev": "concurrently -k -r \"wait-on tcp:5000 && pnpm run dev:backend \" \"pnpm run dev:frontend\"",
|
||||||
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts",
|
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts",
|
||||||
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
|
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
|
||||||
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
|
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
|
||||||
@ -24,7 +25,8 @@
|
|||||||
"reset-password": "tsx ./extra/reset-password.ts"
|
"reset-password": "tsx ./extra/reset-password.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.11",
|
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.12",
|
||||||
|
"@inventage/envsubst": "^0.16.0",
|
||||||
"@louislam/sqlite3": "~15.1.6",
|
"@louislam/sqlite3": "~15.1.6",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"check-password-strength": "~2.0.7",
|
"check-password-strength": "~2.0.7",
|
||||||
@ -33,6 +35,7 @@
|
|||||||
"composerize": "~1.4.1",
|
"composerize": "~1.4.1",
|
||||||
"croner": "~7.0.5",
|
"croner": "~7.0.5",
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.10",
|
||||||
|
"dotenv": "~16.3.1",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"http-graceful-shutdown": "~3.1.13",
|
"http-graceful-shutdown": "~3.1.13",
|
||||||
@ -40,34 +43,37 @@
|
|||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
"knex": "~2.5.1",
|
"knex": "~2.5.1",
|
||||||
"limiter-es6-compat": "~2.1.2",
|
"limiter-es6-compat": "~2.1.2",
|
||||||
"mysql2": "~3.6.3",
|
"mysql2": "~3.6.5",
|
||||||
"promisify-child-process": "~4.1.2",
|
"promisify-child-process": "~4.1.2",
|
||||||
"redbean-node": "~0.3.3",
|
"redbean-node": "~0.3.3",
|
||||||
"socket.io": "~4.7.2",
|
"socket.io": "~4.7.2",
|
||||||
"socket.io-client": "~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",
|
"ts-command-line-args": "~2.5.1",
|
||||||
"tsx": "~3.14.0",
|
"tsx": "~4.6.2",
|
||||||
"type-fest": "~4.3.3",
|
"type-fest": "~4.3.3",
|
||||||
"yaml": "~2.3.4"
|
"yaml": "~2.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.0",
|
||||||
"@fontsource/jetbrains-mono": "^5.0.17",
|
"@fontsource/jetbrains-mono": "^5.0.18",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/bootstrap": "~5.2.9",
|
"@types/bootstrap": "~5.2.10",
|
||||||
"@types/command-exists": "~1.2.3",
|
"@types/command-exists": "~1.2.3",
|
||||||
"@types/express": "~4.17.21",
|
"@types/express": "~4.17.21",
|
||||||
"@types/jsonwebtoken": "~9.0.5",
|
"@types/jsonwebtoken": "~9.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
||||||
"@typescript-eslint/parser": "~6.8.0",
|
"@typescript-eslint/parser": "~6.8.0",
|
||||||
"@vitejs/plugin-vue": "~4.5.0",
|
"@vitejs/plugin-vue": "~4.5.2",
|
||||||
|
"@xterm/addon-fit": "beta",
|
||||||
|
"@xterm/xterm": "beta",
|
||||||
"bootstrap": "5.3.2",
|
"bootstrap": "5.3.2",
|
||||||
"bootstrap-vue-next": "~0.14.10",
|
"bootstrap-vue-next": "~0.14.10",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"eslint": "~8.50.0",
|
"eslint": "~8.50.0",
|
||||||
"eslint-plugin-jsdoc": "~46.8.2",
|
"eslint-plugin-jsdoc": "~46.8.2",
|
||||||
@ -76,16 +82,16 @@
|
|||||||
"sass": "~1.68.0",
|
"sass": "~1.68.0",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"unplugin-vue-components": "~0.25.2",
|
"unplugin-vue-components": "~0.25.2",
|
||||||
"vite": "~5.0.0",
|
"vite": "~5.0.7",
|
||||||
"vite-plugin-compression": "~0.5.1",
|
"vite-plugin-compression": "~0.5.1",
|
||||||
"vue": "~3.3.8",
|
"vue": "~3.3.11",
|
||||||
"vue-eslint-parser": "~9.3.2",
|
"vue-eslint-parser": "~9.3.2",
|
||||||
"vue-i18n": "~9.5.0",
|
"vue-i18n": "~9.5.0",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vue-qrcode": "~2.2.0",
|
"vue-qrcode": "~2.2.0",
|
||||||
"vue-router": "~4.2.5",
|
"vue-router": "~4.2.5",
|
||||||
"vue-toastification": "2.0.0-rc.5",
|
"vue-toastification": "2.0.0-rc.5",
|
||||||
"xterm": "5.4.0-beta.37",
|
"wait-on": "^7.2.0",
|
||||||
"xterm-addon-web-links": "~0.9.0"
|
"xterm-addon-web-links": "~0.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1404
pnpm-lock.yaml
generated
1404
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user