Compare commits

...

49 Commits

Author SHA1 Message Date
ef904444e4 Revert "Update Docker Dompose plugin to 2.23.3 (#303)"
This reverts commit 6a3eebfd57.
2023-12-17 06:13:34 +08:00
ac2a62abb1 Update to 1.3.4 2023-12-16 23:22:27 +08:00
e77ff3622d Translations update from Kuma Weblate (#302) 2023-12-16 23:19:50 +08:00
b5bd9a711a Translated using Weblate (French)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/fr/
2023-12-16 15:18:03 +00:00
442c7fce67 Translated using Weblate (Thai)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/th/
2023-12-16 15:18:03 +00:00
7d55a84aa2 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hant/
2023-12-16 15:18:03 +00:00
22bbba9652 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/uk/
2023-12-16 15:18:03 +00:00
3bc6779af4 Translated using Weblate (Slovenian)
Currently translated at 97.0% (97 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/sl/
2023-12-16 15:18:03 +00:00
3ef2be1c11 Translated using Weblate (Italian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/it/
2023-12-16 15:18:03 +00:00
f6f7283f09 Translated using Weblate (Czech)
Currently translated at 99.0% (99 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/cs/
2023-12-16 15:18:03 +00:00
69e237a676 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/bg/
2023-12-16 15:18:03 +00:00
6a3eebfd57 Update Docker Dompose plugin to 2.23.3 (#303) 2023-12-16 20:51:24 +08:00
3c137122b6 Update reformat-changelog.ts 2023-12-16 17:58:07 +08:00
e2819afce1 Terminal text cols adjusts to terminal container. (#285)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-12-16 17:57:21 +08:00
94ca8a152a Fix: Only adding folders to stack with a compose file. (#299)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-12-15 18:15:47 +08:00
db0add7e4c Fixed envsubst issue (#301) 2023-12-15 18:08:48 +08:00
0f52bb78b8 Update Readme Badge 2023-12-13 21:08:01 +08:00
03c7815b58 Update to 1.3.3 2023-12-10 21:21:14 +08:00
80d5c685e5 Update reformat-changelog.ts 2023-12-10 21:20:08 +08:00
a8482ec8ac Merge pull request #257 from UptimeKumaBot/weblate-dockge-dockge
Translations update from Kuma Weblate
2023-12-10 21:17:22 +08:00
07c52ccebb Update from upstream (#271)
* Fix

* Fix

* Fix logout

* Fix logout

* Fix

* WIP
2023-12-10 21:16:23 +08:00
587d2dcaca Fix URLs with env 2023-12-10 13:17:04 +08:00
5f1f3593fd Add pnpm run dev 2023-12-10 13:16:40 +08:00
316c566c76 Update dependencies 2023-12-10 02:39:39 +08:00
d32bd3937f Fix: .env code change is not reactive (#270) 2023-12-10 02:29:05 +08:00
007eac7b58 Envsubst YAML in order to display the correct values in the UI instead of tags (#268)
* WIP

* Add envsubst

* WIP
2023-12-10 00:59:28 +08:00
b945ddea55 Fix check update (#269)
* Fix check-version.ts
2023-12-10 00:59:05 +08:00
9b6b49947c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/pt_BR/
2023-12-08 15:49:37 +00:00
3ba267a3dc Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hant/
2023-12-08 15:49:37 +00:00
01411f2d7e Translated using Weblate (Swedish)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/sv/
2023-12-08 15:49:37 +00:00
ba51031db6 Translated using Weblate (Russian)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ru/
2023-12-08 15:49:37 +00:00
daa8d12eee Translated using Weblate (German)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/de/
2023-12-08 15:49:37 +00:00
bd58de535e Update README.md (#264)
Correct spelling errors.
2023-12-08 23:49:29 +08:00
8c6bcef987 Stopped "keyword" match from taking \n (#255) 2023-12-07 20:14:16 +08:00
bec5460395 Update README.md 2023-12-06 03:14:14 +08:00
4e899dcf21 Fix: Arabic to RTL 2023-12-05 16:57:10 +08:00
8296c7b18f Update to 1.3.2 2023-12-05 03:01:46 +08:00
607c908f2d Fix #236 2023-12-05 03:01:06 +08:00
bd5dd3c3ad Update to 1.3.1 2023-12-05 02:47:05 +08:00
6eca6dc59f Fix #234 (#235) 2023-12-05 02:41:25 +08:00
54fb2c1ef4 Update to 1.3.0 2023-12-04 23:16:32 +08:00
562abb485d Merge pull request #229 from UptimeKumaBot/weblate-dockge-dockge
Translations update from Kuma Weblate
2023-12-04 19:53:08 +08:00
86bed768ea Translated using Weblate (Korean)
Currently translated at 100.0% (100 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ko/
2023-12-04 10:40:44 +00:00
b79db2375f Translated using Weblate (Italian)
Currently translated at 99.0% (99 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/it/
2023-12-04 10:40:44 +00:00
b586cca711 Translated using Weblate (Japanese)
Currently translated at 96.0% (96 of 100 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ja/
2023-12-04 10:40:44 +00:00
793a9de50d Env follow up (#231)
* Create the env file only if not empty

* Update

* Check some fs operation to async
2023-12-04 18:40:37 +08:00
0df3fee3f4 Minor 2023-12-03 21:57:27 +08:00
05b79ba50e Fix freeze issue (#227)
* Fix freeze issue

* Fix
2023-12-03 21:30:50 +08:00
a3c4082800 Sort non managed stack to the end (#228) 2023-12-03 21:24:06 +08:00
42 changed files with 1747 additions and 643 deletions

View File

@ -1,4 +1,3 @@
title: "❓ Ask for help"
labels: [help]
body:
- type: checkboxes

View File

@ -1,4 +1,3 @@
title: 🚀 Feature Request
labels: [feature-request]
body:
- type: checkboxes

View File

@ -1,14 +1,14 @@
name: " Ask for help"
description: "Please go to the Discussions tab to submit a Help Request"
name: "⚠️ Ask for help (Please go to the \"Discussions\" tab to submit a Help Request)"
description: "⚠️ Please go to the \"Discussions\" tab to submit a Help Request"
body:
- type: markdown
attributes:
value: |
Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
- type: checkboxes
id: no-duplicate-issues
attributes:
label: "Issues are for bug reports only"
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
options:
- label: "I understand"
required: true

View File

@ -1,14 +1,14 @@
name: 🚀 Feature Request
description: "Please go to the Discussions tab to submit a Feature Request"
name: 🚀 Feature Request (Please go to the "Discussions" tab to submit a Feature Request)
description: "⚠️ Please go to the \"Discussions\" tab to submit a Feature Request"
body:
- type: markdown
attributes:
value: |
Please go to https://github.com/louislam/dockge/discussions/new?category=feature-request
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
- type: checkboxes
id: no-duplicate-issues
attributes:
label: "Issues are for bug reports only"
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
options:
- label: "I understand"
required: true

View File

@ -6,7 +6,7 @@
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github) ![GitHub issues](https://img.shields.io/github/issues/louislam/dockge?logo=github) ![GitHub pull requests](https://img.shields.io/github/issues-pr/louislam/dockge?logo=github) ![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge/latest?label=docker%20image%20ver.) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github) ![GitHub](https://img.shields.io/github/license/louislam/dockge?logo=github)
![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github) ![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge/latest?label=docker%20image%20ver.) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github)
<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" 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`.
@ -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
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.
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?

View File

@ -3,69 +3,55 @@ import compareVersions from "compare-versions";
import packageJSON from "../package.json";
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
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
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() {
const check = async () => {
if (await Settings.get("checkUpdate") === false) {
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";
async startInterval() {
const check = async () => {
if (await Settings.get("checkUpdate") === false) {
return;
}
const checkBeta = await Settings.get("checkBeta");
log.debug("update-checker", "Retrieving latest versions");
if (checkBeta && data.beta) {
if (compareVersions.compare(data.beta, data.slow, ">")) {
obj.latestVersion = data.beta;
return;
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");
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 (_) {
log.info("update-checker", "Failed to check for new versions");
}
};
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();
await check();
this.interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
}
}
const checkVersion = new CheckVersion();
export default checkVersion;

View File

@ -1,3 +0,0 @@
export class Docker {
}

View File

@ -29,9 +29,11 @@ import { Stack } from "./stack";
import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user";
import childProcess from "child_process";
import childProcessAsync from "promisify-child-process";
import { Terminal } from "./terminal";
import "dotenv/config";
export class DockgeServer {
app : Express;
httpServer : http.Server;
@ -192,6 +194,39 @@ export class DockgeServer {
// Create Socket.io
this.io = new socketIO.Server(this.httpServer, {
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) => {
@ -306,6 +341,7 @@ export class DockgeServer {
this.sendStackList(true);
});
checkVersion.startInterval();
});
gracefulShutdown(this.httpServer, {
@ -483,7 +519,7 @@ export class DockgeServer {
return jwtSecretBean;
}
sendStackList(useCache = false) {
async sendStackList(useCache = false) {
let roomList = this.io.sockets.adapter.rooms.keys();
let map : Map<string, object> | undefined;
@ -494,7 +530,7 @@ export class DockgeServer {
// Get the list only if there is a room
if (!map) {
map = new Map();
let stackList = Stack.getStackList(this, useCache);
let stackList = await Stack.getStackList(this, useCache);
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON());
@ -510,8 +546,8 @@ export class DockgeServer {
}
}
sendStackStatusList() {
let statusList = Stack.getStatusList();
async sendStackStatusList() {
let statusList = await Stack.getStatusList();
let roomList = this.io.sockets.adapter.rooms.keys();
@ -529,8 +565,15 @@ export class DockgeServer {
}
}
getDockerNetworkList() : string[] {
let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]);
async getDockerNetworkList() : Promise<string[]> {
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return [];
}
let list = res.stdout.toString().split("\n");
// Remove empty string item
@ -568,4 +611,35 @@ export class DockgeServer {
finalFunction() {
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}`;
}
}

View File

@ -12,7 +12,7 @@ export class DockerSocketHandler extends SocketHandler {
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try {
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);
server.sendStackList();
callback({
@ -45,7 +45,7 @@ export class DockerSocketHandler extends SocketHandler {
if (typeof(name) !== "string") {
throw new ValidationError("Name must be a string");
}
const stack = Stack.getStack(server, name);
const stack = await Stack.getStack(server, name);
try {
await stack.delete(socket);
@ -65,7 +65,7 @@ export class DockerSocketHandler extends SocketHandler {
}
});
socket.on("getStack", (stackName : unknown, callback) => {
socket.on("getStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
@ -73,7 +73,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
if (stack.isManagedByDockge) {
stack.joinCombinedTerminal(socket);
@ -111,7 +111,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.start(socket);
callback({
ok: true,
@ -135,7 +135,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.stop(socket);
callback({
ok: true,
@ -156,7 +156,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.restart(socket);
callback({
ok: true,
@ -177,7 +177,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.update(socket);
callback({
ok: true,
@ -198,7 +198,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.down(socket);
callback({
ok: true,
@ -219,7 +219,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName, true);
const stack = await Stack.getStack(server, stackName, true);
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({
ok: true,
@ -234,7 +234,7 @@ export class DockerSocketHandler extends SocketHandler {
socket.on("getDockerNetworkList", async (callback) => {
try {
checkLogin(socket);
const dockerNetworkList = server.getDockerNetworkList();
const dockerNetworkList = await server.getDockerNetworkList();
callback({
ok: true,
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
if (typeof(name) !== "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);
stack.save(isAdd);
await stack.save(isAdd);
return stack;
}

View File

@ -211,6 +211,8 @@ export class MainSocketHandler extends SocketHandler {
let user = await doubleCheckPassword(socket, password.currentPassword);
await user.resetPassword(password.newPassword);
server.disconnectAllSocketClients(user.id, socket.id);
callback({
ok: true,
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> {

View File

@ -101,7 +101,7 @@ export class TerminalSocketHandler extends SocketHandler {
log.debug("interactiveTerminal", "Service name: " + serviceName);
// Get stack
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
stack.joinContainerTerminal(socket, serviceName, shell);
callback({
@ -151,7 +151,7 @@ export class TerminalSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string.");
}
const stack = Stack.getStack(server, stackName);
const stack = await Stack.getStack(server, stackName);
await stack.leaveCombinedTerminal(socket);
callback({
@ -162,9 +162,44 @@ export class TerminalSocketHandler extends SocketHandler {
}
});
// TODO: Resize Terminal
socket.on("terminalResize", async (rows : unknown) => {
// Resize Terminal
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}`
);
}
}
);
}
}

View File

@ -1,10 +1,11 @@
import { DockgeServer } from "./dockge-server";
import fs from "fs";
import fs, { promises as fsAsync } from "fs";
import { log } from "./log";
import yaml from "yaml";
import { DockgeSocket, ValidationError } from "./util-server";
import { DockgeSocket, fileExists, ValidationError } from "./util-server";
import path from "path";
import {
acceptedComposeFileNames,
COMBINED_TERMINAL_COLS,
COMBINED_TERMINAL_ROWS,
CREATED_FILE,
@ -16,7 +17,7 @@ import {
UNKNOWN
} from "./util-common";
import { InteractiveTerminal, Terminal } from "./terminal";
import childProcess from "child_process";
import childProcessAsync from "promisify-child-process";
export class Stack {
@ -40,8 +41,7 @@ export class Stack {
if (!skipFSOperations) {
// 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 supportedFileNames) {
for (const filename of acceptedComposeFileNames) {
if (fs.existsSync(path.join(this.path, filename))) {
this._composeFileName = filename;
break;
@ -72,11 +72,15 @@ export class Stack {
/**
* Get the status of the stack from `docker compose ps --format json`
*/
ps() : object {
let res = childProcess.execSync("docker compose ps --format json", {
cwd: this.path
async ps() : Promise<object> {
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
encoding: "utf-8",
});
return JSON.parse(res.toString());
if (!res.stdout) {
return {};
}
return JSON.parse(res.stdout.toString());
}
get isManagedByDockge() : boolean {
@ -95,6 +99,15 @@ export class Stack {
// Check YAML format
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 {
@ -142,29 +155,35 @@ export class Stack {
* Save the stack to the disk
* @param isAdd
*/
save(isAdd : boolean) {
async save(isAdd : boolean) {
this.validate();
let dir = this.path;
// Check if the name is used if isAdd
if (isAdd) {
if (fs.existsSync(dir)) {
if (await fileExists(dir)) {
throw new ValidationError("Stack name already exists");
}
// Create the stack folder
fs.mkdirSync(dir);
await fsAsync.mkdir(dir);
} else {
if (!fs.existsSync(dir)) {
if (!await fileExists(dir)) {
throw new ValidationError("Stack not found");
}
}
// 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
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> {
@ -184,7 +203,7 @@ export class Stack {
}
// Remove the stack folder
fs.rmSync(this.path, {
await fsAsync.rm(this.path, {
recursive: true,
force: true
});
@ -192,8 +211,8 @@ export class Stack {
return exitCode;
}
updateStatus() {
let statusList = Stack.getStatusList();
async updateStatus() {
let statusList = await Stack.getStatusList();
let status = statusList.get(this.name);
if (status) {
@ -203,7 +222,27 @@ export class Stack {
}
}
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, 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>> {
let stacksDir = server.stacksDir;
let stackList : Map<string, Stack>;
@ -214,16 +253,20 @@ export class Stack {
stackList = new Map<string, Stack>();
// 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) {
try {
// 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()) {
continue;
}
let stack = this.getStack(server, filename);
// If no compose file exists, skip it
if (!await Stack.composeFileExists(stacksDir, filename)) {
continue;
}
let stack = await this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
@ -238,8 +281,15 @@ export class Stack {
}
// Get status from docker compose ls
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return stackList;
}
let composeList = JSON.parse(res.stdout.toString());
for (let composeStack of composeList) {
let stack = stackList.get(composeStack.Name);
@ -265,11 +315,18 @@ export class Stack {
* Get the status list, it will be used to update the status of the stacks
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
*/
static getStatusList() : Map<string, number> {
static async getStatusList() : Promise<Map<string, number>> {
let statusList = new Map<string, number>();
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return statusList;
}
let composeList = JSON.parse(res.stdout.toString());
for (let composeStack of composeList) {
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
@ -297,13 +354,13 @@ export class Stack {
}
}
static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack {
static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> {
let dir = path.join(server.stacksDir, stackName);
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
let stackList = this.getStackList(server, true);
let stackList = await this.getStackList(server, true);
let stack = stackList.get(stackName);
if (stack) {
@ -374,7 +431,7 @@ export class Stack {
}
// If the stack is not running, we don't need to restart it
this.updateStatus();
await this.updateStatus();
log.debug("update", "Status: " + this.status);
if (this.status !== RUNNING) {
return exitCode;
@ -422,24 +479,35 @@ export class Stack {
async getServiceStatusList() {
let statusList = new Map<string, number>();
let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
});
try {
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
encoding: "utf-8",
});
let lines = res.stdout.toString().split("\n");
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) {
if (!res.stdout) {
return statusList;
}
let lines = res.stdout?.toString().split("\n");
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) {
}
}
return statusList;
} catch (e) {
log.error("getServiceStatusList", e);
return statusList;
}
return statusList;
}
}

View File

@ -67,6 +67,7 @@ export class Terminal {
set cols(cols : number) {
this._cols = cols;
log.debug("Terminal", `Terminal cols: ${this._cols}`); // Added to check if cols is being set when changing terminal size.
try {
this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) {

View File

@ -1,13 +1,17 @@
/*
* Common utilities for backend and frontend
*/
import { Document } from "yaml";
import yaml, { Document, Pair, Scalar } from "yaml";
import { DotenvParseOutput } from "dotenv";
// Init dayjs
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import relativeTime from "dayjs/plugin/relativeTime";
// @ts-ignore
import { replaceVariablesSync } from "@inventage/envsubst";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
@ -17,6 +21,11 @@ export interface LooseObject {
[key: string]: any
}
export interface BaseRes {
ok: boolean;
msg?: string;
}
let randomBytes : (numBytes: number) => Uint8Array;
initRandomBytes();
@ -107,6 +116,13 @@ export const allowedRawKeys = [
"\u0003", // Ctrl + C
];
export const acceptedComposeFileNames = [
"compose.yaml",
"docker-compose.yaml",
"docker-compose.yml",
"compose.yml",
];
/**
* Generate a decimal integer number from a string
* @param str Input
@ -340,3 +356,52 @@ export function parseDockerPort(input : string, defaultHostname : string = "loca
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);
}
}

View File

@ -5,6 +5,7 @@ import { log } from "./log";
import { ERROR_TYPE_VALIDATION } from "./util-common";
import { R } from "redbean-node";
import { verifyPassword } from "./password-hash";
import fs from "fs";
export interface JWTDecoded {
username : string;
@ -82,3 +83,9 @@ export async function doubleCheckPassword(socket : DockgeSocket, currentPassword
return user;
}
export function fileExists(file : string) {
return fs.promises.access(file, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

@ -4,16 +4,30 @@ const input = `
`;
const template = `
> [!WARNING]
>
### 🆕 New Features
-
### Improvements
### ⬆️ Improvements
-
### 🐞 Bug Fixes
### 🐛 Bug Fixes
-
### 🦎 Translation Contributions
-
### ⬆️ Security Fixes
-
### Others
- 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() !== "");
@ -37,6 +51,12 @@ for (const line of lines) {
}
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);

View File

@ -4,6 +4,8 @@ import readline from "readline";
import { User } from "../backend/models/user";
import { DockgeServer } from "../backend/dockge-server";
import { log } from "../backend/log";
import { io } from "socket.io-client";
import { BaseRes } from "../backend/util-common";
console.log("== Dockge Reset Password Tool ==");
@ -12,11 +14,10 @@ const rl = readline.createInterface({
output: process.stdout
});
const server = new DockgeServer();
export const main = async () => {
const server = new DockgeServer();
// Check if
console.log("Connecting the database");
try {
await Database.init(server);
@ -47,12 +48,16 @@ export const main = async () => {
// Reset all sessions by reset jwt secret
await server.initJWTSecret();
console.log("Password reset successfully.");
// Disconnect all other socket clients of the user
await disconnectAllSocketClients(user.username, password);
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
}
} catch (e) {
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) {
main();
}

View File

@ -9,7 +9,7 @@
<div v-if="!isEditMode">
<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>
</a>
</div>
@ -213,16 +213,29 @@ export default defineComponent({
jsonObject() {
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() {
if (this.service.image) {
return this.service.image.split(":")[0];
if (this.envsubstService.image) {
return this.envsubstService.image.split(":")[0];
} else {
return "";
}
},
imageTag() {
if (this.service.image) {
let tag = this.service.image.split(":")[1];
if (this.envsubstService.image) {
let tag = this.envsubstService.image.split(":")[1];
if (tag) {
return tag;

View File

@ -152,6 +152,14 @@ export default {
});
result.sort((m1, m2) => {
// sort by managed by dockge
if (m1.isManagedByDockge && !m2.isManagedByDockge) {
return -1;
} else if (!m1.isManagedByDockge && m2.isManagedByDockge) {
return 1;
}
if (m1.status !== m2.status) {
if (m2.status === RUNNING) {
return 1;

View File

@ -5,7 +5,8 @@
</template>
<script>
import { Terminal } from "xterm";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "xterm-addon-web-links";
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() {
window.removeEventListener("resize", this.onResizeEvent); // Remove the resize event listener from the window object.
this.$root.unbindTerminal(this.name);
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);
}
}
};

View File

@ -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
|| languageList[navigator.language] && navigator.language

View File

@ -12,7 +12,7 @@
"registry": "Регистър",
"compose": "Compose",
"addFirstStackMsg": "Създайте вашия първи стак!",
"stackName" : "Име на стак",
"stackName": "Име на стак",
"deployStack": "Разположи",
"deleteStack": "Изтрий",
"stopStack": "Спри",
@ -22,7 +22,7 @@
"editStack": "Редактирай",
"discardStack": "Отхвърли",
"saveStackDraft": "Запази",
"notAvailableShort" : "N/A",
"notAvailableShort": "N/A",
"deleteStackMsg": "Сигурни ли сте, че желаете да изтриете този стак?",
"stackNotManagedByDockgeMsg": "Този стак не се управлява от Dockge.",
"primaryHostname": "Основно име на хост",
@ -90,5 +90,13 @@
"Allowed commands:": "Позволени команди:",
"Internal Networks": "Вътрешни мрежи",
"External Networks": "Външни мрежи",
"No External Networks": "Не са налични външни мрежи"
"No External Networks": "Не са налични външни мрежи",
"reverseProxyMsg2": "Проверете как да го конфигурирате за WebSocket",
"downStack": "Спри и изключи",
"reverseProxyMsg1": "Използвате ревърс прокси?",
"Cannot connect to the socket server.": "Не може да се свърже със сокет сървъра.",
"url": "URL адрес | URL адреси",
"extra": "Допълнително",
"reconnecting...": "Повторно свързване…",
"connecting...": "Свързване със сокет сървъра…"
}

View File

@ -3,39 +3,39 @@
"Create your admin account": "Vytvořit účet administrátora",
"authIncorrectCreds": "Nesprávné uživatelské jméno nebo heslo.",
"PasswordsDoNotMatch": "Hesla se neshodují.",
"Repeat Password": "Opakujte heslo",
"Repeat Password": "Napište Heslo Znovu",
"Create": "Vytvořit",
"signedInDisp": "Přihlášen jako {0}",
"signedInDispDisabled": "Ověření zakázáno.",
"signedInDispDisabled": "Ověření Zakázáno.",
"home": "Domů",
"console": "Konzole",
"registry": "Registry",
"compose": "Compose",
"addFirstStackMsg": "Vytvořte svůj první stack!",
"stackName": "Název stacku",
"compose": "Komponovat",
"addFirstStackMsg": "Vytvořte svůj první zásobník!",
"stackName": "Název Zásobníku",
"deployStack": "Nainstalovat",
"deleteStack": "Smazat",
"stopStack": "Zastavit",
"restartStack": "Restartovat",
"updateStack": "Aktualizovat",
"startStack": "Spustit",
"downStack": "Zastavit a vypnout",
"downStack": "Zastavit & Vypnout",
"editStack": "Upravit",
"discardStack": "Zahodit",
"saveStackDraft": "Uložit",
"notAvailableShort": "N/A",
"deleteStackMsg": "Opravdu chcete smazat tento stack?",
"deleteStackMsg": "Opravdu chcete smazat tento zásobník?",
"stackNotManagedByDockgeMsg": "Tento stack není spravován systémem Dockge.",
"primaryHostname": "Primární název hostitele",
"general": "Obecné",
"container": "Kontejner | Kontejnery",
"scanFolder": "Prohledat složku se stacky",
"scanFolder": "Prohledat složku se zásobníky",
"dockerImage": "Obrázek",
"restartPolicyUnlessStopped": "Pokud není zastaveno",
"restartPolicyAlways": "Vždy",
"restartPolicyOnFailure": "Při selhání",
"restartPolicyOnFailure": "Při Selhání",
"restartPolicyNo": "Ne",
"environmentVariable": "Proměnná prostředí | Proměnné prostředí",
"environmentVariable": "Proměnná Prostředí | Proměnné Prostředí",
"restartPolicy": "Politika restartu",
"containerName": "Název kontejneru",
"port": "Port | Porty",
@ -91,5 +91,11 @@
"Allowed commands:": "Povolené příkazy:",
"Internal Networks": "Interní sítě",
"External Networks": "Externí sítě",
"No External Networks": "Žádné externí sítě"
"No External Networks": "Žádné externí sítě",
"reconnecting...": "Opětovné připojení…",
"url": "Adresa URL | Adresy URL",
"extra": "Extra",
"reverseProxyMsg1": "Používáte Reverzní proxy server?",
"reverseProxyMsg2": "Podívat se jak to nastavit pro WebSocket",
"Cannot connect to the socket server.": "Nelze se připojit k serveru ."
}

View File

@ -10,7 +10,7 @@
"home": "Startseite",
"console": "Konsole",
"registry": "Container Registry",
"compose": "",
"compose": "Compose",
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
"stackName": "Stack-Name",
"deployStack": "Deployen",
@ -72,15 +72,15 @@
"Check Update On GitHub": "Update auf GitHub überprüfen",
"Show update if available": "Update anzeigen, wenn verfügbar",
"Also check beta release": "Auch Beta-Version überprüfen",
"Remember me": "Anmeldung beibehalten",
"Remember me": "Angemeldet bleiben",
"Login": "Anmelden",
"Username": "Benutzername",
"Password": "Passwort",
"Settings": "Einstellungen",
"Logout": "Abmelden",
"Lowercase only": "Nur Kleinbuchstaben",
"Convert to Compose": "In Compose Syntax umwandeln",
"Docker Run": "Docker ausführen",
"Convert to Compose": "In Compose-Syntax umwandeln",
"Docker Run": "Docker Run",
"active": "aktiv",
"exited": "beendet",
"inactive": "inaktiv",

View File

@ -98,5 +98,6 @@
"reconnecting...": "Reconnecting…",
"connecting...": "Connecting to the socket server…",
"url": "URL | URLs",
"extra": "Extra"
"extra": "Extra",
"newUpdate": "New Update"
}

View File

@ -98,5 +98,6 @@
"downStack": "Arrêter et désactiver",
"reverseProxyMsg1": "Utilisez vous un proxy inverse ?",
"Cannot connect to the socket server.": "Impossible de se connecter au serveur socket.",
"reconnecting...": "Reconnexion…"
"reconnecting...": "Reconnexion…",
"newUpdate": "Nouvelle mise à jour"
}

View File

@ -10,10 +10,10 @@
"home": "Home",
"console": "Console",
"registry": "Registro",
"compose": "Compose",
"compose": "Componi",
"addFirstStackMsg": "Componi il tuo primo stack!",
"stackName" : "Nome dello stack",
"deployStack": "Deploy",
"stackName": "Nome dello stack",
"deployStack": "Rilascia",
"deleteStack": "Cancella",
"stopStack": "Stop",
"restartStack": "Riavvia",
@ -23,7 +23,7 @@
"editStack": "Modifica",
"discardStack": "Annulla",
"saveStackDraft": "Salva",
"notAvailableShort" : "N/D",
"notAvailableShort": "N/D",
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
"primaryHostname": "Hostname primario",
@ -75,7 +75,7 @@
"Also check beta release": "Controlla anche le release in beta",
"Remember me": "Ricordami",
"Login": "Login",
"Username": "Username",
"Username": "Nome Utente",
"Password": "Password",
"Settings": "Impostazioni",
"Logout": "Logout",
@ -91,5 +91,12 @@
"Allowed commands:": "Comandi permessi:",
"Internal Networks": "Reti interne",
"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…",
"url": "Indirizzo | Indirizzi"
}

View File

@ -17,7 +17,7 @@
"editStack": "編集",
"discardStack": "破棄",
"saveStackDraft": "保存",
"stackNotManagedByDockgeMsg": "このスタックはDockageによって管理されていません。",
"stackNotManagedByDockgeMsg": "このスタックはDockgeによって管理されていません。",
"general": "一般",
"scanFolder": "スタックフォルダをスキャン",
"dockerImage": "イメージ",

View File

@ -90,5 +90,13 @@
"Allowed commands:": "허용된 명령어:",
"Internal 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...": "재연결 중…"
}

View File

@ -12,7 +12,7 @@
"registry": "Registro",
"compose": "Compose",
"addFirstStackMsg": "Crie sua primeira stack!",
"stackName" : "Nome da stack",
"stackName": "Nome da stack",
"deployStack": "Deploy",
"deleteStack": "Excluir",
"stopStack": "Parar",
@ -22,7 +22,7 @@
"editStack": "Editar",
"discardStack": "Descartar",
"saveStackDraft": "Salvar",
"notAvailableShort" : "N/D",
"notAvailableShort": "N/D",
"deleteStackMsg": "Tem certeza que deseja excluir esta stack?",
"stackNotManagedByDockgeMsg": "Esta stack não é gerenciada pelo Dockge.",
"primaryHostname": "Nome do Host Primário",
@ -90,5 +90,13 @@
"Allowed commands:": "Comandos permitidos:",
"Internal Networks": "Redes internas",
"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…"
}

View File

@ -5,12 +5,12 @@
"PasswordsDoNotMatch": "Пароль не совпадает.",
"Repeat Password": "Повторите пароль",
"Create": "Создать",
"signedInDisp": "Авторизован как",
"signedInDisp": "Авторизован как {0}",
"signedInDispDisabled": "Авторизация выключена.",
"home": "Главная",
"console": "Консоль",
"registry": "Registry",
"compose": "Compose",
"registry": "Реестр (Registry)",
"compose": "Составить (Compose)",
"addFirstStackMsg": "Создайте свой первый стек!",
"stackName": "Имя стека",
"deployStack": "Развернуть",
@ -90,5 +90,13 @@
"Allowed commands:": "Разрешенные команды:",
"Internal Networks": "Внутренние сети",
"External Networks": "Внешние сети",
"No External Networks": "Нет внешних сетей"
"No External Networks": "Нет внешних сетей",
"downStack": "Остановить и выключить",
"reverseProxyMsg1": "Использовать Реверс Прокси?",
"reconnecting...": "Переподключение…",
"Cannot connect to the socket server.": "Не удается подключиться к серверу сокетов.",
"url": "URL адрес(а)",
"extra": "Дополнительно",
"reverseProxyMsg2": "Проверьте, как настроить его для WebSocket",
"connecting...": "Подключение к серверу сокетов…"
}

View File

@ -90,5 +90,10 @@
"Allowed commands:": "Dovoljeni ukazi:",
"Internal Networks": "Notranja omrežja",
"External Networks": "Zunanja omrežja",
"No External Networks": "Ni zunanjih omrežij"
"No External Networks": "Ni zunanjih omrežij",
"downStack": "Ustavi & Odstrani",
"connecting...": "Povezovanje s strežnikom…",
"reverseProxyMsg1": "Uporabljate obratni proxy?",
"extra": "Dodatno",
"reconnecting...": "Ponovna povezava …"
}

View File

@ -1,6 +1,6 @@
{
"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.",
"PasswordsDoNotMatch": "Lösenorden matchar inte.",
"Repeat Password": "Repetera lösenord",
@ -12,28 +12,28 @@
"registry": "Register",
"compose": "Komponera",
"addFirstStackMsg": "Komponera din första stack!",
"stackName" : "Stacknamn",
"stackName": "Stacknamn",
"deployStack": "Distribuera",
"deleteStack": "Radera",
"stopStack": "Stop",
"stopStack": "Stoppa",
"restartStack": "Starta om",
"updateStack": "Uppdatera",
"startStack": "Starta",
"downStack": "Stop & Ner",
"downStack": "Stoppa & Ner",
"editStack": "Redigera",
"discardStack": "Kasta",
"saveStackDraft": "Spara",
"notAvailableShort" : "N/A",
"notAvailableShort": "N/A",
"deleteStackMsg": "Är du säker på att du vill radera stacken?",
"stackNotManagedByDockgeMsg": "Denna stacken hanteras inte av Dockge.",
"primaryHostname": "Primärt värdnamn",
"general": "Allmän",
"container": "Container | Containrar",
"scanFolder": "Scanna Stackfolder",
"dockerImage": "Bild",
"restartPolicyUnlessStopped": "Om inte stoppas",
"scanFolder": "Skanna Stackmapp",
"dockerImage": "Avbild",
"restartPolicyUnlessStopped": "Om inte stoppad",
"restartPolicyAlways": "Alltid",
"restartPolicyOnFailure": "Vid Misslyckande",
"restartPolicyOnFailure": "Vid misslyckande",
"restartPolicyNo": "Nej",
"environmentVariable": "Miljövariabel | Miljövariabler",
"restartPolicy": "Omstartspolicy",
@ -44,12 +44,12 @@
"dependsOn": "Containerberoende | Containerberoenden",
"addListItem": "Lägg till {0}",
"deleteContainer": "Radera",
"addContainer": "Lägg till Container",
"addNetwork": "Lägg till Nätverk",
"addContainer": "Lägg till container",
"addNetwork": "Lägg till nätverk",
"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.",
"passwordNotMatchMsg": "Det upprepade lösenordet matchar inte",
"autoGet": "Auto Hämta",
"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.",
"autoGet": "Auto-hämta",
"add": "Lägg till",
"Edit": "Redigera",
"applyToYAML": "Lägg till i YAML",
@ -57,8 +57,8 @@
"addInternalNetwork": "Lägg till",
"Save": "Spara",
"Language": "Språk",
"Current User": "Nuvarande användaren",
"Change Password": "Byt lösenord",
"Current User": "Nuvarande användare",
"Change Password": "Ändra lösenord",
"Current Password": "Nuvarande lösenord",
"New Password": "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",
"Leave": "Lämna",
"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",
"Also check beta release": "Kontrollera även betaversionen",
"Also check beta release": "Kontrollera även betaversioner",
"Remember me": "Kom ihåg mig",
"Login": "Logga in",
"Username": "Användarnamn",
@ -80,8 +80,8 @@
"Settings": "Inställningar",
"Logout": "Logga ut",
"Lowercase only": "Endast små tecken",
"Convert to Compose": "Omvandla till Compose",
"Docker Run": "Docker Run",
"Convert to Compose": "Omvandla till compose",
"Docker Run": "Docker kör",
"active": "aktiv",
"exited": "avslutad",
"inactive": "inaktiv",
@ -89,7 +89,14 @@
"Security": "Säkerhet",
"About": "Om",
"Allowed commands:": "Tillåtna kommandon:",
"Internal Networks": "Interna Nätverk",
"External Networks": "Externa Nätverk",
"No External Networks": "Inga Externa Nätverk"
"Internal Networks": "Interna nätverk",
"External Networks": "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…"
}

View File

@ -91,5 +91,12 @@
"Allowed commands:": "คำสั่งที่อนุญาต:",
"Internal Networks": "เครือข่ายภายใน",
"External Networks": "เครือข่ายภายนอก",
"No External Networks": "ไม่มีเครือข่ายภายนอก"
"No External Networks": "ไม่มีเครือข่ายภายนอก",
"reverseProxyMsg2": "ตรวจสอบวิธีกำหนดค่าสำหรับ WebSocket",
"Cannot connect to the socket server.": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ socket ได้",
"reverseProxyMsg1": "ใช้ Reverse Proxy หรือไม่?",
"connecting...": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์ socket…",
"url": "URL | URLs",
"extra": "พิเศษ",
"reconnecting...": "กำลังเชื่อมต่อใหม่…"
}

View File

@ -92,7 +92,7 @@
"External Networks": "Зовнішні мережі",
"No External Networks": "Немає зовнішніх мереж",
"downStack": "Зупинити і вимкнути",
"reverseProxyMsg1": "Використовувати зворотній проксі?",
"reverseProxyMsg1": "Використовуєте зворотній проксі?",
"Cannot connect to the socket server.": "Не вдається підключитися до сервера сокетів.",
"reconnecting...": "Повторне підключення…",
"connecting...": "Підключення до сервера сокетів…",

View File

@ -51,7 +51,7 @@
"autoGet": "自動取得",
"add": "新增",
"Edit": "編輯",
"applyToYAML": "用到YAML",
"applyToYAML": "用到 YAML",
"createExternalNetwork": "建立",
"addInternalNetwork": "新增",
"Save": "儲存",
@ -71,7 +71,7 @@
"Frontend Version": "前端版本",
"Check Update On GitHub": "在 GitHub 上檢查更新",
"Show update if available": "有更新時提醒我",
"Also check beta release": "同時檢查 Beta 渠道更新",
"Also check beta release": "同時檢查 Beta 更新",
"Remember me": "記住我",
"Login": "登入",
"Username": "使用者名稱",
@ -92,7 +92,7 @@
"External Networks": "外部網路",
"No External Networks": "無外部網路",
"downStack": "停止",
"reverseProxyMsg1": "在使用反向代理",
"reverseProxyMsg1": "在使用反向代理",
"reverseProxyMsg2": "點擊這裡了解如何為 WebSocket 配置反向代理",
"Cannot connect to the socket server.": "無法連接到 Socket 伺服器。",
"reconnecting...": "重新連線中…",

View File

@ -16,8 +16,8 @@
<span class="fs-4 title">Dockge</span>
</router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-info me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
<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("newUpdate") }}
</a>
<ul class="nav nav-pills">

View File

@ -10,12 +10,12 @@ import { i18n } from "./i18n";
// Dependencies
import "bootstrap";
import Toast, { POSITION, useToast } from "vue-toastification";
import "xterm/lib/xterm.js";
import "@xterm/xterm/lib/xterm.js";
// CSS
import "@fontsource/jetbrains-mono";
import "vue-toastification/dist/index.css";
import "xterm/css/xterm.css";
import "@xterm/xterm/css/xterm.css";
import "./styles/main.scss";
// Minxins

View File

@ -2,7 +2,7 @@ import { io } from "socket.io-client";
import { Socket } from "socket.io-client";
import { defineComponent } from "vue";
import jwtDecode from "jwt-decode";
import { Terminal } from "xterm";
import { Terminal } from "@xterm/xterm";
let socket : Socket;
@ -202,6 +202,10 @@ export default defineComponent({
}
}
});
socket.on("refresh", () => {
location.reload();
});
},
/**

View File

@ -231,7 +231,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
COMBINED_TERMINAL_COLS,
COMBINED_TERMINAL_ROWS,
copyYAMLComments,
copyYAMLComments, envsubstYAML,
getCombinedTerminalName,
getComposeTerminalName,
PROGRESS_TERMINAL_ROWS,
@ -239,6 +239,7 @@ import {
} from "../../../backend/util-common";
import { BModal } from "bootstrap-vue-next";
import NetworkInput from "../components/NetworkInput.vue";
import dotenv from "dotenv";
const template = `version: "3.8"
services:
@ -277,6 +278,7 @@ export default {
return {
editorFocus: false,
jsonConfig: {},
envsubstJSONConfig: {},
yamlError: "",
processing: true,
showProgressTerminal: false,
@ -297,12 +299,12 @@ export default {
computed: {
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 [];
}
let urls = [];
for (const url of this.jsonConfig["x-dockge"].urls) {
for (const url of this.envsubstJSONConfig["x-dockge"].urls) {
let display;
try {
let obj = new URL(url);
@ -372,6 +374,17 @@ export default {
},
deep: true,
},
"stack.composeENV": {
handler() {
if (this.editorFocus) {
console.debug("env code changed");
this.yamlCodeChange();
}
},
deep: true,
},
jsonConfig: {
handler() {
if (!this.editorFocus) {
@ -622,7 +635,7 @@ export default {
greedy: true
},
"keyword": {
pattern: /^[^ :=]*(?=[:=])/m,
pattern: /^\w*(?=[:=])/m,
greedy: true
},
"value": {
@ -645,28 +658,41 @@ export default {
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() {
try {
let doc = parseDocument(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");
}
let { config, doc } = this.yamlToJSON(this.stack.composeYAML);
this.yamlDoc = doc;
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);
this.yamlError = "";
} catch (e) {

View File

@ -1,6 +1,6 @@
{
"name": "dockge",
"version": "1.2.0",
"version": "1.3.4",
"type": "module",
"engines": {
"node": ">= 18.0.0 && <= 18.17.1"
@ -10,6 +10,7 @@
"lint": "eslint \"**/*.{ts,vue}\"",
"check-ts": "tsc --noEmit",
"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: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",
@ -24,7 +25,8 @@
"reset-password": "tsx ./extra/reset-password.ts"
},
"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",
"bcryptjs": "~2.4.3",
"check-password-strength": "~2.0.7",
@ -33,6 +35,7 @@
"composerize": "~1.4.1",
"croner": "~7.0.5",
"dayjs": "~1.11.10",
"dotenv": "~16.3.1",
"express": "~4.18.2",
"express-static-gzip": "~2.1.7",
"http-graceful-shutdown": "~3.1.13",
@ -40,33 +43,37 @@
"jwt-decode": "~3.1.2",
"knex": "~2.5.1",
"limiter-es6-compat": "~2.1.2",
"mysql2": "~3.6.3",
"mysql2": "~3.6.5",
"promisify-child-process": "~4.1.2",
"redbean-node": "~0.3.3",
"socket.io": "~4.7.2",
"socket.io-client": "~4.7.2",
"timezones-list": "~3.0.2",
"ts-command-line-args": "~2.5.1",
"tsx": "~3.14.0",
"tsx": "~4.6.2",
"type-fest": "~4.3.3",
"yaml": "~2.3.4"
},
"devDependencies": {
"@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/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@types/bcryptjs": "^2.4.6",
"@types/bootstrap": "~5.2.9",
"@types/bootstrap": "~5.2.10",
"@types/command-exists": "~1.2.3",
"@types/express": "~4.17.21",
"@types/jsonwebtoken": "~9.0.5",
"@typescript-eslint/eslint-plugin": "~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-vue-next": "~0.14.10",
"concurrently": "^8.2.2",
"cross-env": "~7.0.3",
"eslint": "~8.50.0",
"eslint-plugin-jsdoc": "~46.8.2",
@ -75,16 +82,16 @@
"sass": "~1.68.0",
"typescript": "~5.2.2",
"unplugin-vue-components": "~0.25.2",
"vite": "~5.0.0",
"vite": "~5.0.7",
"vite-plugin-compression": "~0.5.1",
"vue": "~3.3.8",
"vue": "~3.3.11",
"vue-eslint-parser": "~9.3.2",
"vue-i18n": "~9.5.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.5",
"xterm": "5.4.0-beta.37",
"wait-on": "^7.2.0",
"xterm-addon-web-links": "~0.9.0"
}
}

1412
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff