Compare commits

..

64 Commits

Author SHA1 Message Date
Louis Lam
17566fcd95
[README.md] Fix star badge 2024-01-04 03:12:43 +08:00
Louis Lam
ec6bdea711
Fix the remote url is undefined (#338) 2024-01-04 00:43:39 +08:00
Louis Lam
f8ad8c45fd Update to 1.4.0 2024-01-03 20:16:04 +08:00
Louis Lam
c239f40acc
Translations update from Kuma Weblate (#324) 2024-01-03 19:58:50 +08:00
AICHIGUA
8efa58e0d0 Translated using Weblate (Chinese (Simplified))
Currently translated at 88.4% (100 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hans/
2024-01-03 11:22:49 +00:00
Zandor Smith
dbbefa6c09 Translated using Weblate (Dutch)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/nl/
2024-01-02 18:01:29 +00:00
dng-nguyn
a253f8ab25 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/vi/
2024-01-02 18:01:29 +00:00
dng-nguyn
701b0158b1 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/vi/
2024-01-02 18:01:29 +00:00
dng-nguyn
c94eb4805d Added translation using Weblate (Vietnamese) 2024-01-02 18:01:29 +00:00
Louis Lam
4670121dfa Translated using Weblate (Chinese (Traditional))
Currently translated at 90.2% (102 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hant/
2024-01-02 18:01:29 +00:00
Louis Lam
9d5d062420 Translated using Weblate (Chinese (Traditional))
Currently translated at 88.4% (100 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hant/
2024-01-02 18:01:29 +00:00
Marco
21f7a677a3 Translated using Weblate (German)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/de/
2024-01-02 18:01:29 +00:00
demonisius
25026b1ed5 Translated using Weblate (Russian)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ru/
2024-01-02 18:01:29 +00:00
Abner Santana
afe433dbfa Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/pt_BR/
2024-01-02 18:01:29 +00:00
Christian Bergschneider
480c498974 Translated using Weblate (German)
Currently translated at 99.1% (112 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/de/
2024-01-02 18:01:29 +00:00
Marco
8fe75feb69 Translated using Weblate (German)
Currently translated at 99.1% (112 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/de/
2024-01-02 18:01:29 +00:00
stanol
17046b500b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/uk/
2024-01-02 18:01:29 +00:00
Gunnar Norin
75ff8e1d5c Translated using Weblate (Swedish)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/sv/
2024-01-02 18:01:29 +00:00
Alanimdeo
660da44938 Translated using Weblate (Korean)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ko/
2024-01-02 18:01:29 +00:00
Cyril59310
193f975c4c Translated using Weblate (French)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/fr/
2024-01-02 18:01:29 +00:00
Ismael
7f1b03edab Translated using Weblate (Spanish)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/es/
2024-01-02 18:01:29 +00:00
MrEddX
900ab8978f Translated using Weblate (Bulgarian)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/bg/
2024-01-02 18:01:29 +00:00
Louis Lam
11e71f373d Translated using Weblate (Japanese)
Currently translated at 82.3% (93 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ja/
2024-01-02 18:01:29 +00:00
Louis Lam
8bd432a4b6 Translated using Weblate (Chinese (Traditional))
Currently translated at 88.4% (100 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hant/
2024-01-02 18:01:29 +00:00
Louis Lam
7795bcab03 Translated using Weblate (Chinese (Simplified))
Currently translated at 87.6% (99 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/zh_Hans/
2024-01-02 18:01:29 +00:00
Salvatore Cahyo
3ad7302e03 Translated using Weblate (Indonesian)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/id/
2024-01-02 18:01:29 +00:00
Abner Santana
3140947174 Translated using Weblate (Portuguese)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/pt/
2024-01-02 18:01:29 +00:00
Cyril59310
2c7b938f69 Translated using Weblate (French)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/fr/
2024-01-02 18:01:29 +00:00
Raphael
d3a595b02f Translated using Weblate (German)
Currently translated at 89.3% (101 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/de/
2024-01-02 18:01:29 +00:00
Louis Lam
4b93794cda Translated using Weblate (English)
Currently translated at 100.0% (113 of 113 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/en/
2024-01-02 18:01:29 +00:00
Louis Lam
8b8a9d0f1f Also apply the final release to the beta tag and the nightly tag 2024-01-03 02:01:17 +08:00
dng-nguyn
d4546e1a85
Added Vietnamese language (#332) 2024-01-02 03:45:37 +08:00
Louis Lam
b8cff4cc51 Fix the hostname for the current agent 2023-12-30 19:31:23 +08:00
Louis Lam
cc02eee50c
[README.md] Add links to badges 2023-12-27 18:23:27 +08:00
Louis Lam
5578f28456 Update to 1.4.0-beta.0 2023-12-26 19:45:15 +08:00
Louis Lam
11f9302e62 Add Bahasa Indonesia to the list 2023-12-26 19:43:33 +08:00
Louis Lam
012f7f1116
Translations update from Kuma Weblate (#318) 2023-12-26 19:41:39 +08:00
demonisius
ea2b02587b Translated using Weblate (Russian)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/ru/
2023-12-26 11:36:55 +00:00
Aji Priyo Wibowo
91ba7761f9 Translated using Weblate (Indonesian)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/id/
2023-12-26 11:36:55 +00:00
Freddie
91b3165ea8 Translated using Weblate (Danish)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/da/
2023-12-26 11:36:55 +00:00
stanol
f621d9c4c3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/uk/
2023-12-26 11:36:55 +00:00
Gunnar Norin
3e486008bb Translated using Weblate (Swedish)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/sv/
2023-12-26 11:36:55 +00:00
Rumplin
4049ae22f0 Translated using Weblate (Slovenian)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/sl/
2023-12-26 11:36:55 +00:00
leonsk29
df58de180e Translated using Weblate (Spanish)
Currently translated at 100.0% (101 of 101 strings)

Translation: Dockge/dockge
Translate-URL: https://weblate.kuma.pet/projects/dockge/dockge/es/
2023-12-26 11:36:55 +00:00
Aji Priyo Wibowo
07e259db5c Added translation using Weblate (Indonesian) 2023-12-26 11:36:55 +00:00
Louis Lam
1b84531b31
Update README.md 2023-12-26 04:14:23 +08:00
Louis Lam
de2de0573b
Multiple Dockge instances (#200) 2023-12-26 04:12:44 +08:00
Louis Lam
80e885e85d Update to 1.3.5 2023-12-17 06:21:24 +08:00
Louis Lam
e54ede3f1c
Revert "Update Docker Dompose plugin to 2.23.3" (#307) 2023-12-17 06:14:15 +08:00
Louis Lam
ac2a62abb1 Update to 1.3.4 2023-12-16 23:22:27 +08:00
Louis Lam
e77ff3622d
Translations update from Kuma Weblate (#302) 2023-12-16 23:19:50 +08:00
Cyril59310
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
watchakorn-18k
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
Peter Dave Hello
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
stanol
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
Rumplin
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
Alessandro Del Prete
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
JinToSek
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
MrEddX
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
Louis Lam
6a3eebfd57
Update Docker Dompose plugin to 2.23.3 (#303) 2023-12-16 20:51:24 +08:00
Louis Lam
3c137122b6 Update reformat-changelog.ts 2023-12-16 17:58:07 +08:00
Zack Hankin
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
Zack Hankin
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
Louis Lam
db0add7e4c
Fixed envsubst issue (#301) 2023-12-15 18:08:48 +08:00
61 changed files with 2149 additions and 669 deletions

View File

@ -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.
![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) [![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github&style=flat)](https://github.com/louislam/dockge) [![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker)](https://hub.docker.com/r/louislam/dockge/tags) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge/latest?label=docker%20image%20ver.)](https://hub.docker.com/r/louislam/dockge/tags) [![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github)](https://github.com/louislam/dockge/commits/master/)
<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="" />
@ -14,20 +14,19 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
## ⭐ Features ## ⭐ Features
- Manage `compose.yaml` - 🧑‍💼 Manage your `compose.yaml` files
- Create/Edit/Start/Stop/Restart/Delete - Create/Edit/Start/Stop/Restart/Delete
- Update Docker Images - Update Docker Images
- Interactive Editor for `compose.yaml` - ⌨️ Interactive Editor for `compose.yaml`
- Interactive Web Terminal - 🦦 Interactive Web Terminal
- Reactive - 🕷️ (1.4.0 🆕) Multiple agents support - You can manage multiple stacks from different Docker hosts in one single interface
- Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time - 🏪 Convert `docker run ...` commands into `compose.yaml`
- Easy-to-use & fancy UI - 📙 File based structure - Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands
- If you love Uptime Kuma's UI/UX, you will love this one too
- Convert `docker run ...` commands into `compose.yaml`
- File based structure
- Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands
<img src="https://github.com/louislam/dockge/assets/1336778/cc071864-592e-4909-b73a-343a57494002" width=300 />
<img src="https://github.com/louislam/dockge/assets/1336778/cc071864-592e-4909-b73a-343a57494002" width=300 />
- 🚄 Reactive - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time
- 🐣 Easy-to-use & fancy UI - If you love Uptime Kuma's UI/UX, you will love this one too
![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a) ![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a)

291
backend/agent-manager.ts Normal file
View File

@ -0,0 +1,291 @@
import { DockgeSocket } from "./util-server";
import { io, Socket as SocketClient } from "socket.io-client";
import { log } from "./log";
import { Agent } from "./models/agent";
import { isDev, LooseObject, sleep } from "../common/util-common";
import semver from "semver";
import { R } from "redbean-node";
import dayjs, { Dayjs } from "dayjs";
/**
* Dockge Instance Manager
* One AgentManager per Socket connection
*/
export class AgentManager {
protected socket : DockgeSocket;
protected agentSocketList : Record<string, SocketClient> = {};
protected agentLoggedInList : Record<string, boolean> = {};
protected _firstConnectTime : Dayjs = dayjs();
constructor(socket: DockgeSocket) {
this.socket = socket;
}
get firstConnectTime() : Dayjs {
return this._firstConnectTime;
}
test(url : string, username : string, password : string) : Promise<void> {
return new Promise((resolve, reject) => {
let obj = new URL(url);
let endpoint = obj.host;
if (!endpoint) {
reject(new Error("Invalid Dockge URL"));
}
if (this.agentSocketList[endpoint]) {
reject(new Error("The Dockge URL already exists"));
}
let client = io(url, {
reconnection: false,
extraHeaders: {
endpoint,
}
});
client.on("connect", () => {
client.emit("login", {
username: username,
password: password,
}, (res : LooseObject) => {
if (res.ok) {
resolve();
} else {
reject(new Error(res.msg));
}
client.disconnect();
});
});
client.on("connect_error", (err) => {
if (err.message === "xhr poll error") {
reject(new Error("Unable to connect to the Dockge instance"));
} else {
reject(err);
}
client.disconnect();
});
});
}
/**
*
* @param url
* @param username
* @param password
*/
async add(url : string, username : string, password : string) : Promise<Agent> {
let bean = R.dispense("agent") as Agent;
bean.url = url;
bean.username = username;
bean.password = password;
await R.store(bean);
return bean;
}
/**
*
* @param url
*/
async remove(url : string) {
let bean = await R.findOne("agent", " url = ? ", [
url,
]);
if (bean) {
await R.trash(bean);
let endpoint = bean.endpoint;
delete this.agentSocketList[endpoint];
} else {
throw new Error("Agent not found");
}
}
connect(url : string, username : string, password : string) {
let obj = new URL(url);
let endpoint = obj.host;
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "connecting",
});
if (!endpoint) {
log.error("agent-manager", "Invalid endpoint: " + endpoint + " URL: " + url);
return;
}
if (this.agentSocketList[endpoint]) {
log.debug("agent-manager", "Already connected to the socket server: " + endpoint);
return;
}
log.info("agent-manager", "Connecting to the socket server: " + endpoint);
let client = io(url, {
extraHeaders: {
endpoint,
}
});
client.on("connect", () => {
log.info("agent-manager", "Connected to the socket server: " + endpoint);
client.emit("login", {
username: username,
password: password,
}, (res : LooseObject) => {
if (res.ok) {
log.info("agent-manager", "Logged in to the socket server: " + endpoint);
this.agentLoggedInList[endpoint] = true;
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "online",
});
} else {
log.error("agent-manager", "Failed to login to the socket server: " + endpoint);
this.agentLoggedInList[endpoint] = false;
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "offline",
});
}
});
});
client.on("connect_error", (err) => {
log.error("agent-manager", "Error from the socket server: " + endpoint);
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "offline",
});
});
client.on("disconnect", () => {
log.info("agent-manager", "Disconnected from the socket server: " + endpoint);
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "offline",
});
});
client.on("agent", (...args : unknown[]) => {
this.socket.emit("agent", ...args);
});
client.on("info", (res) => {
log.debug("agent-manager", res);
// Disconnect if the version is lower than 1.4.0
if (!isDev && semver.satisfies(res.version, "< 1.4.0")) {
this.socket.emit("agentStatus", {
endpoint: endpoint,
status: "offline",
msg: `${endpoint}: Unsupported version: ` + res.version,
});
client.disconnect();
}
});
this.agentSocketList[endpoint] = client;
}
disconnect(endpoint : string) {
let client = this.agentSocketList[endpoint];
client?.disconnect();
}
async connectAll() {
this._firstConnectTime = dayjs();
if (this.socket.endpoint) {
log.info("agent-manager", "This connection is connected as an agent, skip connectAll()");
return;
}
let list : Record<string, Agent> = await Agent.getAgentList();
if (Object.keys(list).length !== 0) {
log.info("agent-manager", "Connecting to all instance socket server(s)...");
}
for (let endpoint in list) {
let agent = list[endpoint];
this.connect(agent.url, agent.username, agent.password);
}
}
disconnectAll() {
for (let endpoint in this.agentSocketList) {
this.disconnect(endpoint);
}
}
async emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) {
log.debug("agent-manager", "Emitting event to endpoint: " + endpoint);
let client = this.agentSocketList[endpoint];
if (!client) {
log.error("agent-manager", "Socket client not found for endpoint: " + endpoint);
throw new Error("Socket client not found for endpoint: " + endpoint);
}
if (!client.connected || !this.agentLoggedInList[endpoint]) {
// Maybe the request is too quick, the socket is not connected yet, check firstConnectTime
// If it is within 10 seconds, we should apply retry logic here
let diff = dayjs().diff(this.firstConnectTime, "second");
log.debug("agent-manager", endpoint + ": diff: " + diff);
let ok = false;
while (diff < 10) {
if (client.connected && this.agentLoggedInList[endpoint]) {
log.debug("agent-manager", `${endpoint}: Connected & Logged in`);
ok = true;
break;
}
log.debug("agent-manager", endpoint + ": not ready yet, retrying in 1 second...");
await sleep(1000);
diff = dayjs().diff(this.firstConnectTime, "second");
}
if (!ok) {
log.error("agent-manager", `${endpoint}: Socket client not connected`);
throw new Error("Socket client not connected for endpoint: " + endpoint);
}
}
client.emit("agent", endpoint, eventName, ...args);
}
emitToAllEndpoints(eventName: string, ...args : unknown[]) {
log.debug("agent-manager", "Emitting event to all endpoints");
for (let endpoint in this.agentSocketList) {
this.emitToEndpoint(endpoint, eventName, ...args).catch((e) => {
log.warn("agent-manager", e.message);
});
}
}
async sendAgentList() {
let list = await Agent.getAgentList();
let result : Record<string, LooseObject> = {};
// Myself
result[""] = {
url: "",
username: "",
endpoint: "",
};
for (let endpoint in list) {
let agent = list[endpoint];
result[endpoint] = agent.toJSON();
}
this.socket.emit("agentList", {
ok: true,
agentList: result,
});
}
}

View File

@ -0,0 +1,7 @@
import { DockgeServer } from "./dockge-server";
import { AgentSocket } from "../common/agent-socket";
import { DockgeSocket } from "./util-server";
export abstract class AgentSocketHandler {
abstract create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket): void;
}

View File

@ -1,45 +1,44 @@
import { SocketHandler } from "../socket-handler.js"; import { AgentSocketHandler } from "../agent-socket-handler";
import { DockgeServer } from "../dockge-server"; import { DockgeServer } from "../dockge-server";
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server"; import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { Stack } from "../stack"; import { Stack } from "../stack";
import { AgentSocket } from "../../common/agent-socket";
// @ts-ignore export class DockerSocketHandler extends AgentSocketHandler {
import composerize from "composerize"; create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
// Do not call super.create()
export class DockerSocketHandler extends SocketHandler { agentSocket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
create(socket : DockgeSocket, server : DockgeServer) {
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); const stack = await this.saveStack(server, name, composeYAML, composeENV, isAdd);
await stack.deploy(socket); await stack.deploy(socket);
server.sendStackList(); server.sendStackList();
callback({ callbackResult({
ok: true, ok: true,
msg: "Deployed", msg: "Deployed",
}); }, callback);
stack.joinCombinedTerminal(socket); stack.joinCombinedTerminal(socket);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
socket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); await this.saveStack(server, name, composeYAML, composeENV, isAdd);
callback({ callbackResult({
ok: true, ok: true,
"msg": "Saved" "msg": "Saved"
}); }, callback);
server.sendStackList(); server.sendStackList();
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
socket.on("deleteStack", async (name : unknown, callback) => { agentSocket.on("deleteStack", async (name : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
if (typeof(name) !== "string") { if (typeof(name) !== "string") {
@ -55,17 +54,17 @@ export class DockerSocketHandler extends SocketHandler {
} }
server.sendStackList(); server.sendStackList();
callback({ callbackResult({
ok: true, ok: true,
msg: "Deleted" msg: "Deleted"
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
socket.on("getStack", async (stackName : unknown, callback) => { agentSocket.on("getStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -79,31 +78,31 @@ export class DockerSocketHandler extends SocketHandler {
stack.joinCombinedTerminal(socket); stack.joinCombinedTerminal(socket);
} }
callback({ callbackResult({
ok: true, ok: true,
stack: stack.toJSON(), stack: await stack.toJSON(socket.endpoint),
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// requestStackList // requestStackList
socket.on("requestStackList", async (callback) => { agentSocket.on("requestStackList", async (callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
server.sendStackList(); server.sendStackList();
callback({ callbackResult({
ok: true, ok: true,
msg: "Updated" msg: "Updated"
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// startStack // startStack
socket.on("startStack", async (stackName : unknown, callback) => { agentSocket.on("startStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -113,10 +112,10 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.start(socket); await stack.start(socket);
callback({ callbackResult({
ok: true, ok: true,
msg: "Started" msg: "Started"
}); }, callback);
server.sendStackList(); server.sendStackList();
stack.joinCombinedTerminal(socket); stack.joinCombinedTerminal(socket);
@ -127,7 +126,7 @@ export class DockerSocketHandler extends SocketHandler {
}); });
// stopStack // stopStack
socket.on("stopStack", async (stackName : unknown, callback) => { agentSocket.on("stopStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -137,10 +136,10 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.stop(socket); await stack.stop(socket);
callback({ callbackResult({
ok: true, ok: true,
msg: "Stopped" msg: "Stopped"
}); }, callback);
server.sendStackList(); server.sendStackList();
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
@ -148,7 +147,7 @@ export class DockerSocketHandler extends SocketHandler {
}); });
// restartStack // restartStack
socket.on("restartStack", async (stackName : unknown, callback) => { agentSocket.on("restartStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -158,10 +157,10 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.restart(socket); await stack.restart(socket);
callback({ callbackResult({
ok: true, ok: true,
msg: "Restarted" msg: "Restarted"
}); }, callback);
server.sendStackList(); server.sendStackList();
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
@ -169,7 +168,7 @@ export class DockerSocketHandler extends SocketHandler {
}); });
// updateStack // updateStack
socket.on("updateStack", async (stackName : unknown, callback) => { agentSocket.on("updateStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -179,10 +178,10 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.update(socket); await stack.update(socket);
callback({ callbackResult({
ok: true, ok: true,
msg: "Updated" msg: "Updated"
}); }, callback);
server.sendStackList(); server.sendStackList();
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
@ -190,7 +189,7 @@ export class DockerSocketHandler extends SocketHandler {
}); });
// down stack // down stack
socket.on("downStack", async (stackName : unknown, callback) => { agentSocket.on("downStack", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -200,10 +199,10 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.down(socket); await stack.down(socket);
callback({ callbackResult({
ok: true, ok: true,
msg: "Downed" msg: "Downed"
}); }, callback);
server.sendStackList(); server.sendStackList();
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
@ -211,7 +210,7 @@ export class DockerSocketHandler extends SocketHandler {
}); });
// Services status // Services status
socket.on("serviceStatusList", async (stackName : unknown, callback) => { agentSocket.on("serviceStatusList", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -221,50 +220,31 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName, true); const stack = await Stack.getStack(server, stackName, true);
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({ callbackResult({
ok: true, ok: true,
serviceStatusList, serviceStatusList,
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// getExternalNetworkList // getExternalNetworkList
socket.on("getDockerNetworkList", async (callback) => { agentSocket.on("getDockerNetworkList", async (callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
const dockerNetworkList = await server.getDockerNetworkList(); const dockerNetworkList = await server.getDockerNetworkList();
callback({ callbackResult({
ok: true, ok: true,
dockerNetworkList, dockerNetworkList,
}); }, callback);
} catch (e) {
callbackError(e, callback);
}
});
// composerize
socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(dockerRunCommand) !== "string") {
throw new ValidationError("dockerRunCommand must be a string");
}
const composeTemplate = composerize(dockerRunCommand);
callback({
ok: true,
composeTemplate,
});
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
} }
async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise<Stack> { async saveStack(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");

View File

@ -1,24 +1,15 @@
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server"; import { DockgeServer } from "../dockge-server";
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server"; import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { log } from "../log"; import { log } from "../log";
import yaml from "yaml";
import path from "path";
import fs from "fs";
import {
allowedCommandList,
allowedRawKeys,
getComposeTerminalName, getContainerExecTerminalName,
isDev,
PROGRESS_TERMINAL_ROWS
} from "../util-common";
import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal"; import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal";
import { Stack } from "../stack"; import { Stack } from "../stack";
import { AgentSocketHandler } from "../agent-socket-handler";
import { AgentSocket } from "../../common/agent-socket";
export class TerminalSocketHandler extends SocketHandler { export class TerminalSocketHandler extends AgentSocketHandler {
create(socket : DockgeSocket, server : DockgeServer) { create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => { agentSocket.on("terminalInput", async (terminalName : unknown, cmd : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -38,17 +29,12 @@ export class TerminalSocketHandler extends SocketHandler {
throw new Error("Terminal not found or it is not a Interactive Terminal."); throw new Error("Terminal not found or it is not a Interactive Terminal.");
} }
} catch (e) { } catch (e) {
if (e instanceof Error) { callbackError(e, callback);
errorCallback({
ok: false,
msg: e.message,
});
}
} }
}); });
// Main Terminal // Main Terminal
socket.on("mainTerminal", async (terminalName : unknown, callback) => { agentSocket.on("mainTerminal", async (terminalName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -59,29 +45,29 @@ export class TerminalSocketHandler extends SocketHandler {
throw new ValidationError("Terminal name must be a string."); throw new ValidationError("Terminal name must be a string.");
} }
log.debug("deployStack", "Terminal name: " + terminalName); log.debug("mainTerminal", "Terminal name: " + terminalName);
let terminal = Terminal.getTerminal(terminalName); let terminal = Terminal.getTerminal(terminalName);
if (!terminal) { if (!terminal) {
terminal = new MainTerminal(server, terminalName); terminal = new MainTerminal(server, terminalName);
terminal.rows = 50; terminal.rows = 50;
log.debug("deployStack", "Terminal created"); log.debug("mainTerminal", "Terminal created");
} }
terminal.join(socket); terminal.join(socket);
terminal.start(); terminal.start();
callback({ callbackResult({
ok: true, ok: true,
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// Interactive Terminal for containers // Interactive Terminal for containers
socket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => { agentSocket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -104,16 +90,16 @@ export class TerminalSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
stack.joinContainerTerminal(socket, serviceName, shell); stack.joinContainerTerminal(socket, serviceName, shell);
callback({ callbackResult({
ok: true, ok: true,
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// Join Output Terminal // Join Output Terminal
socket.on("terminalJoin", async (terminalName : unknown, callback) => { agentSocket.on("terminalJoin", async (terminalName : unknown, callback) => {
if (typeof(callback) !== "function") { if (typeof(callback) !== "function") {
log.debug("console", "Callback is not a function."); log.debug("console", "Callback is not a function.");
return; return;
@ -141,7 +127,7 @@ export class TerminalSocketHandler extends SocketHandler {
}); });
// Leave Combined Terminal // Leave Combined Terminal
socket.on("leaveCombinedTerminal", async (stackName : unknown, callback) => { agentSocket.on("leaveCombinedTerminal", async (stackName : unknown, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
@ -154,17 +140,48 @@ export class TerminalSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.leaveCombinedTerminal(socket); await stack.leaveCombinedTerminal(socket);
callback({ callbackResult({
ok: true, ok: true,
}); }, callback);
} catch (e) { } catch (e) {
callbackError(e, callback); callbackError(e, callback);
} }
}); });
// TODO: Resize Terminal // Resize Terminal
socket.on("terminalResize", async (rows : unknown) => { agentSocket.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

@ -9,7 +9,7 @@ import knex from "knex";
import Dialect from "knex/lib/dialects/sqlite3/index.js"; import Dialect from "knex/lib/dialects/sqlite3/index.js";
import sqlite from "@louislam/sqlite3"; import sqlite from "@louislam/sqlite3";
import { sleep } from "./util-common"; import { sleep } from "../common/util-common";
interface DBConfig { interface DBConfig {
type?: "sqlite" | "mysql"; type?: "sqlite" | "mysql";

View File

@ -1,3 +1,4 @@
import "dotenv/config";
import { MainRouter } from "./routers/main-router"; import { MainRouter } from "./routers/main-router";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { PackageJson } from "type-fest"; import { PackageJson } from "type-fest";
@ -17,23 +18,26 @@ import { Settings } from "./settings";
import checkVersion from "./check-version"; import checkVersion from "./check-version";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { R } from "redbean-node"; import { R } from "redbean-node";
import { genSecret, isDev } from "./util-common"; import { genSecret, isDev, LooseObject } from "../common/util-common";
import { generatePasswordHash } from "./password-hash"; import { generatePasswordHash } from "./password-hash";
import { Bean } from "redbean-node/dist/bean"; import { Bean } from "redbean-node/dist/bean";
import { Arguments, Config, DockgeSocket } from "./util-server"; import { Arguments, Config, DockgeSocket } from "./util-server";
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler"; import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
import expressStaticGzip from "express-static-gzip"; import expressStaticGzip from "express-static-gzip";
import path from "path"; import path from "path";
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler"; import { TerminalSocketHandler } from "./agent-socket-handlers/terminal-socket-handler";
import { Stack } from "./stack"; import { Stack } from "./stack";
import { Cron } from "croner"; import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown"; import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user"; import User from "./models/user";
import childProcessAsync from "promisify-child-process"; import childProcessAsync from "promisify-child-process";
import { AgentManager } from "./agent-manager";
import { AgentProxySocketHandler } from "./socket-handlers/agent-proxy-socket-handler";
import { AgentSocketHandler } from "./agent-socket-handler";
import { AgentSocket } from "../common/agent-socket";
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
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;
@ -50,10 +54,19 @@ export class DockgeServer {
]; ];
/** /**
* List of socket handlers * List of socket handlers (no agent support)
*/ */
socketHandlerList : SocketHandler[] = [ socketHandlerList : SocketHandler[] = [
new MainSocketHandler(), new MainSocketHandler(),
new ManageAgentSocketHandler(),
];
agentProxySocketHandler = new AgentProxySocketHandler();
/**
* List of socket handlers (support agent)
*/
agentSocketHandlerList : AgentSocketHandler[] = [
new DockerSocketHandler(), new DockerSocketHandler(),
new TerminalSocketHandler(), new TerminalSocketHandler(),
]; ];
@ -196,7 +209,7 @@ export class DockgeServer {
cors, cors,
allowRequest: (req, callback) => { allowRequest: (req, callback) => {
let isOriginValid = true; let isOriginValid = true;
const bypass = isDev; const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
if (!bypass) { if (!bypass) {
let host = req.headers.host; let host = req.headers.host;
@ -230,20 +243,52 @@ export class DockgeServer {
}); });
this.io.on("connection", async (socket: Socket) => { this.io.on("connection", async (socket: Socket) => {
log.info("server", "Socket connected!"); let dockgeSocket = socket as DockgeSocket;
dockgeSocket.instanceManager = new AgentManager(dockgeSocket);
dockgeSocket.emitAgent = (event : string, ...args : unknown[]) => {
let obj = args[0];
if (typeof(obj) === "object") {
let obj2 = obj as LooseObject;
obj2.endpoint = dockgeSocket.endpoint;
}
dockgeSocket.emit("agent", event, ...args);
};
this.sendInfo(socket, true); if (typeof(socket.request.headers.endpoint) === "string") {
dockgeSocket.endpoint = socket.request.headers.endpoint;
} else {
dockgeSocket.endpoint = "";
}
if (dockgeSocket.endpoint) {
log.info("server", "Socket connected (agent), as endpoint " + dockgeSocket.endpoint);
} else {
log.info("server", "Socket connected (direct)");
}
this.sendInfo(dockgeSocket, true);
if (this.needSetup) { if (this.needSetup) {
log.info("server", "Redirect to setup page"); log.info("server", "Redirect to setup page");
socket.emit("setup"); dockgeSocket.emit("setup");
} }
// Create socket handlers // Create socket handlers (original, no agent support)
for (const socketHandler of this.socketHandlerList) { for (const socketHandler of this.socketHandlerList) {
socketHandler.create(socket as DockgeSocket, this); socketHandler.create(dockgeSocket, this);
} }
// Create Agent Socket
let agentSocket = new AgentSocket();
// Create agent socket handlers
for (const socketHandler of this.agentSocketHandlerList) {
socketHandler.create(dockgeSocket, this, agentSocket);
}
// Create agent proxy socket handlers
this.agentProxySocketHandler.create2(dockgeSocket, this, agentSocket);
// *************************** // ***************************
// Better do anything after added all socket handlers here // Better do anything after added all socket handlers here
// *************************** // ***************************
@ -251,12 +296,18 @@ export class DockgeServer {
log.debug("auth", "check auto login"); log.debug("auth", "check auto login");
if (await Settings.get("disableAuth")) { if (await Settings.get("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin"); log.info("auth", "Disabled Auth: auto login to admin");
this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User); this.afterLogin(dockgeSocket, await R.findOne("user") as User);
socket.emit("autoLogin"); dockgeSocket.emit("autoLogin");
} else { } else {
log.debug("auth", "need auth"); log.debug("auth", "need auth");
} }
// Socket disconnect
dockgeSocket.on("disconnect", () => {
log.info("server", "Socket disconnected!");
dockgeSocket.instanceManager.disconnectAll();
});
}); });
this.io.on("disconnect", () => { this.io.on("disconnect", () => {
@ -281,6 +332,11 @@ export class DockgeServer {
} catch (e) { } catch (e) {
log.error("server", e); log.error("server", e);
} }
socket.instanceManager.sendAgentList();
// Also connect to other dockge instances
socket.instanceManager.connectAll();
} }
/** /**
@ -519,26 +575,34 @@ export class DockgeServer {
return jwtSecretBean; return jwtSecretBean;
} }
/**
* Send stack list to all connected sockets
* @param useCache
*/
async sendStackList(useCache = false) { async sendStackList(useCache = false) {
let roomList = this.io.sockets.adapter.rooms.keys(); let socketList = this.io.sockets.sockets.values();
let map : Map<string, object> | undefined;
let stackList;
for (let socket of socketList) {
let dockgeSocket = socket as DockgeSocket;
for (let room of roomList) {
// Check if the room is a number (user id) // Check if the room is a number (user id)
if (Number(room)) { if (dockgeSocket.userID) {
// Get the list only if there is a room // Get the list only if there is a logged in user
if (!map) { if (!stackList) {
map = new Map(); stackList = await Stack.getStackList(this, useCache);
let stackList = await Stack.getStackList(this, useCache);
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON());
}
} }
log.debug("server", "Send stack list to room " + room); let map : Map<string, object> = new Map();
this.io.to(room).emit("stackList", {
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON(dockgeSocket.endpoint));
}
log.debug("server", "Send stack list to user: " + dockgeSocket.id + " (" + dockgeSocket.endpoint + ")");
dockgeSocket.emitAgent("stackList", {
ok: true, ok: true,
stackList: Object.fromEntries(map), stackList: Object.fromEntries(map),
}); });
@ -546,25 +610,6 @@ export class DockgeServer {
} }
} }
async sendStackStatusList() {
let statusList = await Stack.getStatusList();
let roomList = this.io.sockets.adapter.rooms.keys();
for (let room of roomList) {
// Check if the room is a number (user id)
if (Number(room)) {
log.debug("server", "Send stack status list to room " + room);
this.io.to(room).emit("stackStatusList", {
ok: true,
stackStatusList: Object.fromEntries(statusList),
});
} else {
log.debug("server", "Skip sending stack status list to room " + room);
}
}
}
async getDockerNetworkList() : Promise<string[]> { async getDockerNetworkList() : Promise<string[]> {
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], { let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
encoding: "utf-8", encoding: "utf-8",
@ -618,10 +663,10 @@ export class DockgeServer {
* @param {string} userID * @param {string} userID
* @param {string?} currentSocketID * @param {string?} currentSocketID
*/ */
disconnectAllSocketClients(userID: number, currentSocketID? : string) { disconnectAllSocketClients(userID: number | undefined, currentSocketID? : string) {
for (const rawSocket of this.io.sockets.sockets.values()) { for (const rawSocket of this.io.sockets.sockets.values()) {
let socket = rawSocket as DockgeSocket; let socket = rawSocket as DockgeSocket;
if (socket.userID === userID && socket.id !== currentSocketID) { if ((!userID || socket.userID === userID) && socket.id !== currentSocketID) {
try { try {
socket.emit("refresh"); socket.emit("refresh");
socket.disconnect(); socket.disconnect();

View File

@ -1,6 +1,6 @@
// Console colors // Console colors
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color // https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
import { intHash, isDev } from "./util-common"; import { intHash, isDev } from "../common/util-common";
import dayjs from "dayjs"; import dayjs from "dayjs";
export const CONSOLE_STYLE_Reset = "\x1b[0m"; export const CONSOLE_STYLE_Reset = "\x1b[0m";

View File

@ -0,0 +1,16 @@
import { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// Create the user table
return knex.schema.createTable("agent", (table) => {
table.increments("id");
table.string("url", 255).notNullable().unique();
table.string("username", 255).notNullable();
table.string("password", 255).notNullable();
table.boolean("active").notNullable().defaultTo(true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable("agent");
}

31
backend/models/agent.ts Normal file
View File

@ -0,0 +1,31 @@
import { BeanModel } from "redbean-node/dist/bean-model";
import { R } from "redbean-node";
import { LooseObject } from "../../common/util-common";
export class Agent extends BeanModel {
static async getAgentList() : Promise<Record<string, Agent>> {
let list = await R.findAll("agent") as Agent[];
let result : Record<string, Agent> = {};
for (let agent of list) {
result[agent.endpoint] = agent;
}
return result;
}
get endpoint() : string {
let obj = new URL(this.url);
return obj.host;
}
toJSON() : LooseObject {
return {
url: this.url,
username: this.username,
endpoint: this.endpoint,
};
}
}
export default Agent;

View File

@ -1,6 +1,6 @@
import { R } from "redbean-node"; import { R } from "redbean-node";
import { log } from "./log"; import { log } from "./log";
import { LooseObject } from "./util-common"; import { LooseObject } from "../common/util-common";
export class Settings { export class Settings {

View File

@ -0,0 +1,47 @@
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
import { checkLogin, DockgeSocket } from "../util-server";
import { AgentSocket } from "../../common/agent-socket";
import { ALL_ENDPOINTS } from "../../common/util-common";
export class AgentProxySocketHandler extends SocketHandler {
create2(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
// Agent - proxying requests if needed
socket.on("agent", async (endpoint : unknown, eventName : unknown, ...args : unknown[]) => {
try {
checkLogin(socket);
// Check Type
if (typeof(endpoint) !== "string") {
throw new Error("Endpoint must be a string: " + endpoint);
}
if (typeof(eventName) !== "string") {
throw new Error("Event name must be a string");
}
if (endpoint === ALL_ENDPOINTS) { // Send to all endpoints
log.debug("agent", "Sending to all endpoints: " + eventName);
socket.instanceManager.emitToAllEndpoints(eventName, ...args);
} else if (!endpoint || endpoint === socket.endpoint) { // Direct connection or matching endpoint
log.debug("agent", "Matched endpoint: " + eventName);
agentSocket.call(eventName, ...args);
} else {
log.debug("agent", "Proxying request to " + endpoint + " for " + eventName);
await socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args);
}
} catch (e) {
if (e instanceof Error) {
log.warn("agent", e.message);
}
}
});
}
create(socket : DockgeSocket, server : DockgeServer) {
throw new Error("Method not implemented. Please use create2 instead.");
}
}

View File

@ -1,3 +1,5 @@
// @ts-ignore
import composerize from "composerize";
import { SocketHandler } from "../socket-handler.js"; import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server"; import { DockgeServer } from "../dockge-server";
import { log } from "../log"; import { log } from "../log";
@ -5,7 +7,14 @@ import { R } from "redbean-node";
import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
import { User } from "../models/user"; import { User } from "../models/user";
import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server"; import {
callbackError,
checkLogin,
DockgeSocket,
doubleCheckPassword,
JWTDecoded,
ValidationError
} from "../util-server";
import { passwordStrength } from "check-password-strength"; import { passwordStrength } from "check-password-strength";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Settings } from "../settings"; import { Settings } from "../settings";
@ -262,8 +271,6 @@ export class MainSocketHandler extends SocketHandler {
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket, currentPassword);
} }
console.log(data);
await Settings.setSettings("general", data); await Settings.setSettings("general", data);
callback({ callback({
@ -294,6 +301,25 @@ export class MainSocketHandler extends SocketHandler {
} }
} }
}); });
// composerize
socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(dockerRunCommand) !== "string") {
throw new ValidationError("dockerRunCommand must be a string");
}
const composeTemplate = composerize(dockerRunCommand);
callback({
ok: true,
composeTemplate,
});
} catch (e) {
callbackError(e, callback);
}
});
} }
async login(username : string, password : string) : Promise<User | null> { async login(username : string, password : string) : Promise<User | null> {

View File

@ -0,0 +1,70 @@
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
import { callbackError, callbackResult, checkLogin, DockgeSocket } from "../util-server";
import { LooseObject } from "../../common/util-common";
export class ManageAgentSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {
// addAgent
socket.on("addAgent", async (requestData : unknown, callback : unknown) => {
try {
log.debug("manage-agent-socket-handler", "addAgent");
checkLogin(socket);
if (typeof(requestData) !== "object") {
throw new Error("Data must be an object");
}
let data = requestData as LooseObject;
let manager = socket.instanceManager;
await manager.test(data.url, data.username, data.password);
await manager.add(data.url, data.username, data.password);
// connect to the agent
manager.connect(data.url, data.username, data.password);
// Refresh another sockets
// It is a bit difficult to control another browser sessions to connect/disconnect agents, so force them to refresh the page will be easier.
server.disconnectAllSocketClients(undefined, socket.id);
manager.sendAgentList();
callbackResult({
ok: true,
msg: "agentAddedSuccessfully",
msgi18n: true,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});
// removeAgent
socket.on("removeAgent", async (url : unknown, callback : unknown) => {
try {
log.debug("manage-agent-socket-handler", "removeAgent");
checkLogin(socket);
if (typeof(url) !== "string") {
throw new Error("URL must be a string");
}
let manager = socket.instanceManager;
await manager.remove(url);
server.disconnectAllSocketClients(undefined, socket.id);
manager.sendAgentList();
callbackResult({
ok: true,
msg: "agentRemovedSuccessfully",
msgi18n: true,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});
}
}

View File

@ -5,6 +5,7 @@ import yaml from "yaml";
import { DockgeSocket, fileExists, 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,
@ -14,9 +15,10 @@ import {
PROGRESS_TERMINAL_ROWS, PROGRESS_TERMINAL_ROWS,
RUNNING, TERMINAL_ROWS, RUNNING, TERMINAL_ROWS,
UNKNOWN UNKNOWN
} from "./util-common"; } from "../common/util-common";
import { InteractiveTerminal, Terminal } from "./terminal"; import { InteractiveTerminal, Terminal } from "./terminal";
import childProcessAsync from "promisify-child-process"; import childProcessAsync from "promisify-child-process";
import { Settings } from "./settings";
export class Stack { export class Stack {
@ -40,8 +42,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;
@ -50,22 +51,41 @@ export class Stack {
} }
} }
toJSON() : object { async toJSON(endpoint : string) : Promise<object> {
let obj = this.toSimpleJSON();
// Since we have multiple agents now, embed primary hostname in the stack object too.
let primaryHostname = await Settings.get("primaryHostname");
if (!primaryHostname) {
if (!endpoint) {
primaryHostname = "localhost";
} else {
// Use the endpoint as the primary hostname
try {
primaryHostname = (new URL("https://" + endpoint).hostname);
} catch (e) {
// Just in case if the endpoint is in a incorrect format
primaryHostname = "localhost";
}
}
}
let obj = this.toSimpleJSON(endpoint);
return { return {
...obj, ...obj,
composeYAML: this.composeYAML, composeYAML: this.composeYAML,
composeENV: this.composeENV, composeENV: this.composeENV,
primaryHostname,
}; };
} }
toSimpleJSON() : object { toSimpleJSON(endpoint : string) : object {
return { return {
name: this.name, name: this.name,
status: this._status, status: this._status,
tags: [], tags: [],
isManagedByDockge: this.isManagedByDockge, isManagedByDockge: this.isManagedByDockge,
composeFileName: this._composeFileName, composeFileName: this._composeFileName,
endpoint,
}; };
} }
@ -186,8 +206,8 @@ export class Stack {
} }
} }
async deploy(socket? : DockgeSocket) : Promise<number> { async deploy(socket : DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to deploy, please check the terminal output for more information."); throw new Error("Failed to deploy, please check the terminal output for more information.");
@ -195,8 +215,8 @@ export class Stack {
return exitCode; return exitCode;
} }
async delete(socket?: DockgeSocket) : Promise<number> { async delete(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information."); throw new Error("Failed to delete, please check the terminal output for more information.");
@ -222,6 +242,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>;
@ -242,6 +282,10 @@ export class Stack {
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);
@ -364,7 +408,7 @@ export class Stack {
} }
async start(socket: DockgeSocket) { async start(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to start, please check the terminal output for more information."); throw new Error("Failed to start, please check the terminal output for more information.");
@ -373,7 +417,7 @@ export class Stack {
} }
async stop(socket: DockgeSocket) : Promise<number> { async stop(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to stop, please check the terminal output for more information."); throw new Error("Failed to stop, please check the terminal output for more information.");
@ -382,7 +426,7 @@ export class Stack {
} }
async restart(socket: DockgeSocket) : Promise<number> { async restart(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to restart, please check the terminal output for more information."); throw new Error("Failed to restart, please check the terminal output for more information.");
@ -391,7 +435,7 @@ export class Stack {
} }
async down(socket: DockgeSocket) : Promise<number> { async down(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to down, please check the terminal output for more information."); throw new Error("Failed to down, please check the terminal output for more information.");
@ -400,7 +444,7 @@ export class Stack {
} }
async update(socket: DockgeSocket) { async update(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path);
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("Failed to pull, please check the terminal output for more information."); throw new Error("Failed to pull, please check the terminal output for more information.");
@ -421,7 +465,7 @@ export class Stack {
} }
async joinCombinedTerminal(socket: DockgeSocket) { async joinCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name); const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
terminal.enableKeepAlive = true; terminal.enableKeepAlive = true;
terminal.rows = COMBINED_TERMINAL_ROWS; terminal.rows = COMBINED_TERMINAL_ROWS;
@ -431,7 +475,7 @@ export class Stack {
} }
async leaveCombinedTerminal(socket: DockgeSocket) { async leaveCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name); const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
const terminal = Terminal.getTerminal(terminalName); const terminal = Terminal.getTerminal(terminalName);
if (terminal) { if (terminal) {
terminal.leave(socket); terminal.leave(socket);
@ -439,7 +483,7 @@ export class Stack {
} }
async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) { async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
const terminalName = getContainerExecTerminalName(this.name, serviceName, index); const terminalName = getContainerExecTerminalName(socket.endpoint, this.name, serviceName, index);
let terminal = Terminal.getTerminal(terminalName); let terminal = Terminal.getTerminal(terminalName);
if (!terminal) { if (!terminal) {

View File

@ -8,7 +8,7 @@ import {
PROGRESS_TERMINAL_ROWS, PROGRESS_TERMINAL_ROWS,
TERMINAL_COLS, TERMINAL_COLS,
TERMINAL_ROWS TERMINAL_ROWS
} from "./util-common"; } from "../common/util-common";
import { sync as commandExistsSync } from "command-exists"; import { sync as commandExistsSync } from "command-exists";
import { log } from "./log"; import { log } from "./log";
@ -34,6 +34,9 @@ export class Terminal {
public enableKeepAlive : boolean = false; public enableKeepAlive : boolean = false;
protected keepAliveInterval? : NodeJS.Timeout; protected keepAliveInterval? : NodeJS.Timeout;
protected kickDisconnectedClientsInterval? : NodeJS.Timeout;
protected socketList : Record<string, DockgeSocket> = {};
constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) { constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
this.server = server; this.server = server;
@ -67,6 +70,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) {
@ -81,13 +85,22 @@ export class Terminal {
return; return;
} }
this.kickDisconnectedClientsInterval = setInterval(() => {
for (const socketID in this.socketList) {
const socket = this.socketList[socketID];
if (!socket.connected) {
log.debug("Terminal", "Kicking disconnected client " + socket.id + " from terminal " + this.name);
this.leave(socket);
}
}
}, 60 * 1000);
if (this.enableKeepAlive) { if (this.enableKeepAlive) {
log.debug("Terminal", "Keep alive enabled for terminal " + this.name); log.debug("Terminal", "Keep alive enabled for terminal " + this.name);
// Close if there is no clients // Close if there is no clients
this.keepAliveInterval = setInterval(() => { this.keepAliveInterval = setInterval(() => {
const clients = this.server.io.sockets.adapter.rooms.get(this.name); const numClients = Object.keys(this.socketList).length;
const numClients = clients ? clients.size : 0;
if (numClients === 0) { if (numClients === 0) {
log.debug("Terminal", "Terminal " + this.name + " has no client, closing..."); log.debug("Terminal", "Terminal " + this.name + " has no client, closing...");
@ -111,8 +124,10 @@ export class Terminal {
// On Data // On Data
this._ptyProcess.onData((data) => { this._ptyProcess.onData((data) => {
this.buffer.pushItem(data); this.buffer.pushItem(data);
if (this.server.io) {
this.server.io.to(this.name).emit("terminalWrite", this.name, data); for (const socketID in this.socketList) {
const socket = this.socketList[socketID];
socket.emitAgent("terminalWrite", this.name, data);
} }
}); });
@ -136,15 +151,19 @@ export class Terminal {
* @param res * @param res
*/ */
protected exit = (res : {exitCode: number, signal?: number | undefined}) => { protected exit = (res : {exitCode: number, signal?: number | undefined}) => {
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode); for (const socketID in this.socketList) {
const socket = this.socketList[socketID];
socket.emitAgent("terminalExit", this.name, res.exitCode);
}
// Remove room // Remove all clients
this.server.io.in(this.name).socketsLeave(this.name); this.socketList = {};
Terminal.terminalMap.delete(this.name); Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
clearInterval(this.keepAliveInterval); clearInterval(this.keepAliveInterval);
clearInterval(this.kickDisconnectedClientsInterval);
if (this.callback) { if (this.callback) {
this.callback(res.exitCode); this.callback(res.exitCode);
@ -156,11 +175,11 @@ export class Terminal {
} }
public join(socket : DockgeSocket) { public join(socket : DockgeSocket) {
socket.join(this.name); this.socketList[socket.id] = socket;
} }
public leave(socket : DockgeSocket) { public leave(socket : DockgeSocket) {
socket.leave(this.name); delete this.socketList[socket.id];
} }
public get ptyProcess() { public get ptyProcess() {

View File

@ -2,10 +2,11 @@ import { Socket } from "socket.io";
import { Terminal } from "./terminal"; import { Terminal } from "./terminal";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { log } from "./log"; import { log } from "./log";
import { ERROR_TYPE_VALIDATION } from "./util-common"; import { ERROR_TYPE_VALIDATION } from "../common/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"; import fs from "fs";
import { AgentManager } from "./agent-manager";
export interface JWTDecoded { export interface JWTDecoded {
username : string; username : string;
@ -15,6 +16,9 @@ export interface JWTDecoded {
export interface DockgeSocket extends Socket { export interface DockgeSocket extends Socket {
userID: number; userID: number;
consoleTerminal? : Terminal; consoleTerminal? : Terminal;
instanceManager : AgentManager;
endpoint : string;
emitAgent : (eventName : string, ...args : unknown[]) => void;
} }
// For command line arguments, so they are nullable // For command line arguments, so they are nullable
@ -56,18 +60,28 @@ export function callbackError(error : unknown, callback : unknown) {
callback({ callback({
ok: false, ok: false,
msg: error.message, msg: error.message,
msgi18n: true,
}); });
} else if (error instanceof ValidationError) { } else if (error instanceof ValidationError) {
callback({ callback({
ok: false, ok: false,
type: ERROR_TYPE_VALIDATION, type: ERROR_TYPE_VALIDATION,
msg: error.message, msg: error.message,
msgi18n: true,
}); });
} else { } else {
log.debug("console", "Unknown error: " + error); log.debug("console", "Unknown error: " + error);
} }
} }
export function callbackResult(result : unknown, callback : unknown) {
if (typeof(callback) !== "function") {
log.error("console", "Callback is not a function");
return;
}
callback(result);
}
export async function doubleCheckPassword(socket : DockgeSocket, currentPassword : unknown) { export async function doubleCheckPassword(socket : DockgeSocket, currentPassword : unknown) {
if (typeof currentPassword !== "string") { if (typeof currentPassword !== "string") {
throw new Error("Wrong data type?"); throw new Error("Wrong data type?");

15
common/agent-socket.ts Normal file
View File

@ -0,0 +1,15 @@
export class AgentSocket {
eventList : Map<string, (...args : unknown[]) => void> = new Map();
on(event : string, callback : (...args : unknown[]) => void) {
this.eventList.set(event, callback);
}
call(eventName : string, ...args : unknown[]) {
const callback = this.eventList.get(eventName);
if (callback) {
callback(...args);
}
}
}

View File

@ -43,6 +43,8 @@ async function initRandomBytes() {
} }
} }
export const ALL_ENDPOINTS = "##ALL_DOCKGE_ENDPOINTS##";
// Stack Status // Stack Status
export const UNKNOWN = 0; export const UNKNOWN = 0;
export const CREATED_FILE = 1; export const CREATED_FILE = 1;
@ -116,6 +118,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
@ -199,20 +208,20 @@ export function getCryptoRandomInt(min: number, max: number):number {
} }
} }
export function getComposeTerminalName(stack : string) { export function getComposeTerminalName(endpoint : string, stack : string) {
return "compose-" + stack; return "compose-" + endpoint + "-" + stack;
} }
export function getCombinedTerminalName(stack : string) { export function getCombinedTerminalName(endpoint : string, stack : string) {
return "combined-" + stack; return "combined-" + endpoint + "-" + stack;
} }
export function getContainerTerminalName(container : string) { export function getContainerTerminalName(endpoint : string, container : string) {
return "container-" + container; return "container-" + endpoint + "-" + container;
} }
export function getContainerExecTerminalName(stackName : string, container : string, index : number) { export function getContainerExecTerminalName(endpoint : string, stackName : string, container : string, index : number) {
return "container-exec-" + stackName + "-" + container + "-" + index; return "container-exec-" + endpoint + "-" + stackName + "-" + container + "-" + index;
} }
export function copyYAMLComments(doc : Document, src : Document) { export function copyYAMLComments(doc : Document, src : Document) {
@ -282,10 +291,9 @@ function copyYAMLCommentsItems(items : any, srcItems : any) {
* - "127.0.0.1:5000-5010:5000-5010" * - "127.0.0.1:5000-5010:5000-5010"
* - "6060:6060/udp" * - "6060:6060/udp"
* @param input * @param input
* @param defaultHostname * @param hostname
*/ */
export function parseDockerPort(input : string, defaultHostname : string = "localhost") { export function parseDockerPort(input : string, hostname : string) {
let hostname = defaultHostname;
let port; let port;
let display; let display;
@ -398,3 +406,4 @@ function traverseYAML(pair : Pair, env : DotenvParseOutput) : void {
pair.value.value = envsubst(pair.value.value, env); pair.value.value = envsubst(pair.value.value, env);
} }
} }

View File

@ -1,6 +1,10 @@
// Generate on GitHub // Generate on GitHub
const input = ` const input = `
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86 * Fixed envsubst issue by @louislam in https://github.com/louislam/dockge/pull/301
* Fix: Only adding folders to stack with a compose file. by @Ozy-Viking in https://github.com/louislam/dockge/pull/299
* Terminal text cols adjusts to terminal container. by @Ozy-Viking in https://github.com/louislam/dockge/pull/285
* Update Docker Dompose plugin to 2.23.3 by @louislam in https://github.com/louislam/dockge/pull/303
* Translations update from Kuma Weblate by @UptimeKumaBot in https://github.com/louislam/dockge/pull/302
`; `;
const template = ` const template = `
@ -9,17 +13,23 @@ const template = `
> >
### 🆕 New Features ### 🆕 New Features
-
### Improvements ### Improvements
-
### 🐛 Bug Fixes ### 🐛 Bug Fixes
-
### 🦎 Translation Contributions ### 🦎 Translation Contributions
-
### Security Fixes ### 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. 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.
`; `;

View File

@ -5,7 +5,7 @@ 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 { io } from "socket.io-client";
import { BaseRes } from "../backend/util-common"; import { BaseRes } from "../common/util-common";
console.log("== Dockge Reset Password Tool =="); console.log("== Dockge Reset Password Tool ==");
@ -92,7 +92,6 @@ function disconnectAllSocketClients(username : string, password : string) : Prom
// Disconnect all socket connections // Disconnect all socket connections
const socket = io(url, { const socket = io(url, {
transports: [ "websocket" ],
reconnection: false, reconnection: false,
timeout: 5000, timeout: 5000,
}); });

View File

@ -137,7 +137,7 @@
<script> <script>
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { parseDockerPort } from "../../../backend/util-common"; import { parseDockerPort } from "../../../common/util-common";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -189,14 +189,34 @@ export default defineComponent({
}, },
terminalRouteLink() { terminalRouteLink() {
return { if (this.endpoint) {
name: "containerTerminal", return {
params: { name: "containerTerminalEndpoint",
stackName: this.stackName, params: {
serviceName: this.name, endpoint: this.endpoint,
type: "bash", stackName: this.stackName,
}, serviceName: this.name,
}; type: "bash",
},
};
} else {
return {
name: "containerTerminal",
params: {
stackName: this.stackName,
serviceName: this.name,
type: "bash",
},
};
}
},
endpoint() {
return this.$parent.$parent.endpoint;
},
stack() {
return this.$parent.$parent.stack;
}, },
stackName() { stackName() {
@ -254,8 +274,12 @@ export default defineComponent({
}, },
methods: { methods: {
parsePort(port) { parsePort(port) {
let hostname = this.$root.info.primaryHostname || location.hostname; if (this.stack.endpoint) {
return parseDockerPort(port, hostname); return parseDockerPort(port, this.stack.primaryHostname);
} else {
let hostname = this.$root.info.primaryHostname || location.hostname;
return parseDockerPort(port, hostname);
}
}, },
remove() { remove() {
delete this.jsonObject.services[this.name]; delete this.jsonObject.services[this.name];

View File

@ -65,6 +65,10 @@ export default {
editorFocus() { editorFocus() {
return this.$parent.$parent.editorFocus; return this.$parent.$parent.editorFocus;
}, },
endpoint() {
return this.$parent.$parent.endpoint;
},
}, },
watch: { watch: {
"jsonConfig.networks": { "jsonConfig.networks": {
@ -134,7 +138,7 @@ export default {
}, },
loadExternalNetworkList() { loadExternalNetworkList() {
this.$root.getSocket().emit("getDockerNetworkList", (res) => { this.$root.emitAgent(this.endpoint, "getDockerNetworkList", (res) => {
if (res.ok) { if (res.ok) {
this.externalNetworkList = res.dockerNetworkList.filter((n) => { this.externalNetworkList = res.dockerNetworkList.filter((n) => {
// Filter out this stack networks // Filter out this stack networks

View File

@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle"> <div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
<div v-if="Object.keys($root.stackList).length === 0" class="text-center mt-3"> <div v-if="Object.keys(sortedStackList).length === 0" class="text-center mt-3">
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link> <router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
</div> </div>
@ -67,7 +67,7 @@
<script> <script>
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
import StackListItem from "../components/StackListItem.vue"; import StackListItem from "../components/StackListItem.vue";
import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../backend/util-common"; import { CREATED_FILE, CREATED_STACK, EXITED, RUNNING, UNKNOWN } from "../../../common/util-common";
export default { export default {
components: { components: {
@ -120,7 +120,7 @@ export default {
* @returns {Array} The sorted list of stacks. * @returns {Array} The sorted list of stacks.
*/ */
sortedStackList() { sortedStackList() {
let result = Object.values(this.$root.stackList); let result = Object.values(this.$root.completeStackList);
result = result.filter(stack => { result = result.filter(stack => {
// filter by search text // filter by search text
@ -160,6 +160,7 @@ export default {
return 1; return 1;
} }
// sort by status
if (m1.status !== m2.status) { if (m1.status !== m2.status) {
if (m2.status === RUNNING) { if (m2.status === RUNNING) {
return 1; return 1;

View File

@ -1,12 +1,14 @@
<template> <template>
<router-link :to="`/compose/${stack.name}`" :class="{ 'dim' : !stack.isManagedByDockge }" class="item"> <router-link :to="url" :class="{ 'dim' : !stack.isManagedByDockge }" class="item">
<Uptime :stack="stack" :fixed-width="true" class="me-2" /> <Uptime :stack="stack" :fixed-width="true" class="me-2" />
<span class="title">{{ stackName }}</span> <div class="title">
<span>{{ stackName }}</span>
<div v-if="$root.agentCount > 1" class="endpoint">{{ endpointDisplay }}</div>
</div>
</router-link> </router-link>
</template> </template>
<script> <script>
import Uptime from "./Uptime.vue"; import Uptime from "./Uptime.vue";
export default { export default {
@ -51,6 +53,16 @@ export default {
}; };
}, },
computed: { computed: {
endpointDisplay() {
return this.$root.endpointDisplayFunction(this.stack.endpoint);
},
url() {
if (this.stack.endpoint) {
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
} else {
return `/compose/${this.stack.name}`;
}
},
depthMargin() { depthMargin() {
return { return {
marginLeft: `${31 * this.depth}px`, marginLeft: `${31 * this.depth}px`,
@ -117,16 +129,31 @@ export default {
padding-right: 2px !important; padding-right: 2px !important;
} }
// .stack-item { .item {
// width: 100%; text-decoration: none;
// }
.tags {
margin-top: 4px;
padding-left: 67px;
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
gap: 0; min-height: 52px;
border-radius: 10px;
transition: all ease-in-out 0.15s;
width: 100%;
padding: 5px 8px;
&.disabled {
opacity: 0.3;
}
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
.title {
margin-top: -4px;
}
.endpoint {
font-size: 12px;
color: $dark-font-color3;
}
} }
.collapsed { .collapsed {

View File

@ -5,9 +5,9 @@
</template> </template>
<script> <script>
import { Terminal } from "xterm"; import { Terminal } from "@xterm/xterm";
import { WebLinksAddon } from "xterm-addon-web-links"; import { FitAddon } from "@xterm/addon-fit";
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common"; import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../common/util-common";
export default { export default {
/** /**
@ -23,6 +23,11 @@ export default {
require: true, require: true,
}, },
endpoint: {
type: String,
require: true,
},
// Require if mode is interactive // Require if mode is interactive
stackName: { stackName: {
type: String, type: String,
@ -109,37 +114,39 @@ export default {
// Create a new Terminal // Create a new Terminal
if (this.mode === "mainTerminal") { if (this.mode === "mainTerminal") {
this.$root.getSocket().emit("mainTerminal", this.name, (res) => { this.$root.emitAgent(this.endpoint, "mainTerminal", this.name, (res) => {
if (!res.ok) { if (!res.ok) {
this.$root.toastRes(res); this.$root.toastRes(res);
} }
}); });
} else if (this.mode === "interactive") { } else if (this.mode === "interactive") {
console.debug("Create Interactive terminal:", this.name); console.debug("Create Interactive terminal:", this.name);
this.$root.getSocket().emit("interactiveTerminal", this.stackName, this.serviceName, this.shell, (res) => { this.$root.emitAgent(this.endpoint, "interactiveTerminal", this.stackName, this.serviceName, this.shell, (res) => {
if (!res.ok) { if (!res.ok) {
this.$root.toastRes(res); this.$root.toastRes(res);
} }
}); });
} }
// 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();
}, },
methods: { methods: {
bind(name) { bind(endpoint, name) {
// Workaround: normally this.name should be set, but it is not sometimes, so we use the parameter, but eventually this.name and name must be the same name // Workaround: normally this.name should be set, but it is not sometimes, so we use the parameter, but eventually this.name and name must be the same name
if (name) { if (name) {
this.$root.unbindTerminal(name); this.$root.unbindTerminal(name);
this.$root.bindTerminal(name, this.terminal); this.$root.bindTerminal(endpoint, name, this.terminal);
console.debug("Terminal bound via parameter: " + name); console.debug("Terminal bound via parameter: " + name);
} else if (this.name) { } else if (this.name) {
this.$root.unbindTerminal(this.name); this.$root.unbindTerminal(this.name);
this.$root.bindTerminal(this.name, this.terminal); this.$root.bindTerminal(this.endpoint, this.name, this.terminal);
console.debug("Terminal bound: " + this.name); console.debug("Terminal bound: " + this.name);
} else { } else {
console.debug("Terminal name not set"); console.debug("Terminal name not set");
@ -170,7 +177,7 @@ export default {
// Remove the input from the terminal // Remove the input from the terminal
this.removeInput(); this.removeInput();
this.$root.getSocket().emit("terminalInput", this.name, buffer + e.key, (err) => { this.$root.emitAgent(this.endpoint, "terminalInput", this.name, buffer + e.key, (err) => {
this.$root.toastError(err.msg); this.$root.toastError(err.msg);
}); });
@ -189,7 +196,7 @@ export default {
// TODO // TODO
} else if (e.key === "\u0003") { // Ctrl + C } else if (e.key === "\u0003") { // Ctrl + C
console.debug("Ctrl + C"); console.debug("Ctrl + C");
this.$root.getSocket().emit("terminalInput", this.name, e.key); this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key);
this.removeInput(); this.removeInput();
} else { } else {
this.cursorPosition++; this.cursorPosition++;
@ -202,12 +209,36 @@ export default {
interactiveTerminalConfig() { interactiveTerminalConfig() {
this.terminal.onKey(e => { this.terminal.onKey(e => {
this.$root.getSocket().emit("terminalInput", this.name, e.key, (res) => { this.$root.emitAgent(this.endpoint, "terminalInput", this.name, e.key, (res) => {
if (!res.ok) { if (!res.ok) {
this.$root.toastRes(res); this.$root.toastRes(res);
} }
}); });
}); });
},
/**
* 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.emitAgent(this.endpoint, "terminalResize", this.name, rows, cols);
} }
} }
}; };

View File

@ -3,7 +3,7 @@
</template> </template>
<script> <script>
import { statusColor, statusNameShort } from "../../../backend/util-common"; import { statusColor, statusNameShort } from "../../../common/util-common";
export default { export default {
props: { props: {

View File

@ -27,6 +27,8 @@ const languageList = {
"ja": "日本語", "ja": "日本語",
"nl": "Nederlands", "nl": "Nederlands",
"ro": "Română", "ro": "Română",
"id": "Bahasa Indonesia (Indonesian)",
"vi": "Tiếng Việt",
}; };
let messages = { let messages = {

View File

@ -12,7 +12,7 @@
"registry": "Регистър", "registry": "Регистър",
"compose": "Compose", "compose": "Compose",
"addFirstStackMsg": "Създайте вашия първи стак!", "addFirstStackMsg": "Създайте вашия първи стак!",
"stackName" : "Име на стак", "stackName": "Име на стак",
"deployStack": "Разположи", "deployStack": "Разположи",
"deleteStack": "Изтрий", "deleteStack": "Изтрий",
"stopStack": "Спри", "stopStack": "Спри",
@ -22,7 +22,7 @@
"editStack": "Редактирай", "editStack": "Редактирай",
"discardStack": "Отхвърли", "discardStack": "Отхвърли",
"saveStackDraft": "Запази", "saveStackDraft": "Запази",
"notAvailableShort" : "N/A", "notAvailableShort": "N/A",
"deleteStackMsg": "Сигурни ли сте, че желаете да изтриете този стак?", "deleteStackMsg": "Сигурни ли сте, че желаете да изтриете този стак?",
"stackNotManagedByDockgeMsg": "Този стак не се управлява от Dockge.", "stackNotManagedByDockgeMsg": "Този стак не се управлява от Dockge.",
"primaryHostname": "Основно име на хост", "primaryHostname": "Основно име на хост",
@ -90,5 +90,26 @@
"Allowed commands:": "Позволени команди:", "Allowed commands:": "Позволени команди:",
"Internal Networks": "Вътрешни мрежи", "Internal Networks": "Вътрешни мрежи",
"External 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...": "Свързване със сокет сървъра…",
"newUpdate": "Нова актуализация",
"currentEndpoint": "Текущ",
"dockgeURL": "Dockge URL адрес (напр. http://127.0.0.1:5001)",
"agentOnline": "Онлайн",
"agentOffline": "Офлайн",
"connect": "Свържи",
"addAgent": "Добави агент",
"agentAddedSuccessfully": "Агентът е добавен успешно.",
"removeAgent": "Премахни агент",
"removeAgentMsg": "Сигурни ли сте, че желаете да премахнете този агент?",
"dockgeAgent": "Dockge агент | Dockge агенти",
"connecting": "Свързване",
"agentRemovedSuccessfully": "Агентът е премахнат успешно."
} }

View File

@ -3,39 +3,39 @@
"Create your admin account": "Vytvořit účet administrátora", "Create your admin account": "Vytvořit účet administrátora",
"authIncorrectCreds": "Nesprávné uživatelské jméno nebo heslo.", "authIncorrectCreds": "Nesprávné uživatelské jméno nebo heslo.",
"PasswordsDoNotMatch": "Hesla se neshodují.", "PasswordsDoNotMatch": "Hesla se neshodují.",
"Repeat Password": "Opakujte heslo", "Repeat Password": "Napište Heslo Znovu",
"Create": "Vytvořit", "Create": "Vytvořit",
"signedInDisp": "Přihlášen jako {0}", "signedInDisp": "Přihlášen jako {0}",
"signedInDispDisabled": "Ověření zakázáno.", "signedInDispDisabled": "Ověření Zakázáno.",
"home": "Domů", "home": "Domů",
"console": "Konzole", "console": "Konzole",
"registry": "Registry", "registry": "Registry",
"compose": "Compose", "compose": "Komponovat",
"addFirstStackMsg": "Vytvořte svůj první stack!", "addFirstStackMsg": "Vytvořte svůj první zásobník!",
"stackName": "Název stacku", "stackName": "Název Zásobníku",
"deployStack": "Nainstalovat", "deployStack": "Nainstalovat",
"deleteStack": "Smazat", "deleteStack": "Smazat",
"stopStack": "Zastavit", "stopStack": "Zastavit",
"restartStack": "Restartovat", "restartStack": "Restartovat",
"updateStack": "Aktualizovat", "updateStack": "Aktualizovat",
"startStack": "Spustit", "startStack": "Spustit",
"downStack": "Zastavit a vypnout", "downStack": "Zastavit & Vypnout",
"editStack": "Upravit", "editStack": "Upravit",
"discardStack": "Zahodit", "discardStack": "Zahodit",
"saveStackDraft": "Uložit", "saveStackDraft": "Uložit",
"notAvailableShort": "N/A", "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.", "stackNotManagedByDockgeMsg": "Tento stack není spravován systémem Dockge.",
"primaryHostname": "Primární název hostitele", "primaryHostname": "Primární název hostitele",
"general": "Obecné", "general": "Obecné",
"container": "Kontejner | Kontejnery", "container": "Kontejner | Kontejnery",
"scanFolder": "Prohledat složku se stacky", "scanFolder": "Prohledat složku se zásobníky",
"dockerImage": "Obrázek", "dockerImage": "Obrázek",
"restartPolicyUnlessStopped": "Pokud není zastaveno", "restartPolicyUnlessStopped": "Pokud není zastaveno",
"restartPolicyAlways": "Vždy", "restartPolicyAlways": "Vždy",
"restartPolicyOnFailure": "Při selhání", "restartPolicyOnFailure": "Při Selhání",
"restartPolicyNo": "Ne", "restartPolicyNo": "Ne",
"environmentVariable": "Proměnná prostředí | Proměnné prostředí", "environmentVariable": "Proměnná Prostředí | Proměnné Prostředí",
"restartPolicy": "Politika restartu", "restartPolicy": "Politika restartu",
"containerName": "Název kontejneru", "containerName": "Název kontejneru",
"port": "Port | Porty", "port": "Port | Porty",
@ -91,5 +91,11 @@
"Allowed commands:": "Povolené příkazy:", "Allowed commands:": "Povolené příkazy:",
"Internal Networks": "Interní sítě", "Internal Networks": "Interní sítě",
"External Networks": "Externí 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

@ -98,5 +98,6 @@
"disableauth.message1": "Er du sikker på, at du vil <strong>deaktivere godkendelse</strong>?", "disableauth.message1": "Er du sikker på, at du vil <strong>deaktivere godkendelse</strong>?",
"disableauth.message2": "Det er designet til scenarier <strong>hvor du har til hensigt at implementere tredjepartsgodkendelse</strong> foran Dockge såsom Cloudflare Access, Authelia eller andre godkendelsesmekanismer.", "disableauth.message2": "Det er designet til scenarier <strong>hvor du har til hensigt at implementere tredjepartsgodkendelse</strong> foran Dockge såsom Cloudflare Access, Authelia eller andre godkendelsesmekanismer.",
"Show update if available": "Vis opdatering, hvis tilgængelig", "Show update if available": "Vis opdatering, hvis tilgængelig",
"Lowercase only": "Kun små bogstaver" "Lowercase only": "Kun små bogstaver",
"newUpdate": "Ny Opdatering"
} }

View File

@ -94,9 +94,22 @@
"Cannot connect to the socket server.": "Keine Verbindung zum Socket Server.", "Cannot connect to the socket server.": "Keine Verbindung zum Socket Server.",
"reverseProxyMsg1": "Wird ein Reverse Proxy genutzt?", "reverseProxyMsg1": "Wird ein Reverse Proxy genutzt?",
"reconnecting...": "Erneuter Verbindungsaufbau…", "reconnecting...": "Erneuter Verbindungsaufbau…",
"downStack": "Stoppen & Aus", "downStack": "Stopp & Inaktiv",
"extra": "Extra", "extra": "Extra",
"url": "URL / URLs", "url": "URL / URLs",
"reverseProxyMsg2": "Lerne wie dieser für WebSockets zu konfigurieren ist.", "reverseProxyMsg2": "Lerne wie dieser für WebSockets zu konfigurieren ist.",
"connecting...": "Verbindungsaufbau zum Socket Server…" "connecting...": "Verbindungsaufbau zum Socket Server…",
"newUpdate": "Neues Update",
"dockgeAgent": "Dockge Agent | Dockge Agenten",
"currentEndpoint": "Aktuell",
"dockgeURL": "Dockge URL (z. B. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Verbinden",
"connect": "Verbinden",
"addAgent": "Agent Hinzufügen",
"agentAddedSuccessfully": "Agent erfolgreich hinzugefügt.",
"agentRemovedSuccessfully": "Agent erfolgreich entfernt.",
"removeAgent": "Agent Entfernen",
"removeAgentMsg": "Bist Du sicher, dass Du diesen Agent entfernen möchtest?"
} }

View File

@ -19,7 +19,7 @@
"restartStack": "Restart", "restartStack": "Restart",
"updateStack": "Update", "updateStack": "Update",
"startStack": "Start", "startStack": "Start",
"downStack": "Stop & Down", "downStack": "Stop & Inactive",
"editStack": "Edit", "editStack": "Edit",
"discardStack": "Discard", "discardStack": "Discard",
"saveStackDraft": "Save", "saveStackDraft": "Save",
@ -99,5 +99,17 @@
"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" "newUpdate": "New Update",
"dockgeAgent": "Dockge Agent | Dockge Agents",
"currentEndpoint": "Current",
"dockgeURL": "Dockge URL (e.g. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Connecting",
"connect": "Connect",
"addAgent": "Add Agent",
"agentAddedSuccessfully": "Agent added successfully.",
"agentRemovedSuccessfully": "Agent removed successfully.",
"removeAgent": "Remove Agent",
"removeAgentMsg": "Are you sure you want to remove this agent?"
} }

View File

@ -12,7 +12,7 @@
"registry": "Registro", "registry": "Registro",
"compose": "Componer", "compose": "Componer",
"addFirstStackMsg": "¡Compón tu primera pila!", "addFirstStackMsg": "¡Compón tu primera pila!",
"stackName" : "Nombre de la Pila", "stackName": "Nombre de la Pila",
"deployStack": "Desplegar", "deployStack": "Desplegar",
"deleteStack": "Eliminar", "deleteStack": "Eliminar",
"stopStack": "Detener", "stopStack": "Detener",
@ -22,7 +22,7 @@
"editStack": "Editar", "editStack": "Editar",
"discardStack": "Descartar", "discardStack": "Descartar",
"saveStackDraft": "Guardar", "saveStackDraft": "Guardar",
"notAvailableShort" : "N/D", "notAvailableShort": "N/D",
"deleteStackMsg": "¿Estás seguro de que quieres eliminar esta pila?", "deleteStackMsg": "¿Estás seguro de que quieres eliminar esta pila?",
"stackNotManagedByDockgeMsg": "Esta pila no está gestionada por Dockge.", "stackNotManagedByDockgeMsg": "Esta pila no está gestionada por Dockge.",
"primaryHostname": "Nombre de Host Primario", "primaryHostname": "Nombre de Host Primario",
@ -90,5 +90,26 @@
"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": "Sin Redes Externas" "No External Networks": "Sin Redes Externas",
"reverseProxyMsg1": "¿Usando un proxy inverso?",
"reverseProxyMsg2": "Compruebe cómo configurarlo para WebSocket",
"newUpdate": "Nueva actualización",
"downStack": "Detener y desactivar",
"Cannot connect to the socket server.": "No se puede conectar al servidor del socket.",
"reconnecting...": "Reconectando…",
"connecting...": "Conectando al servidor del socket…",
"url": "URL | URLs",
"extra": "Addicional",
"currentEndpoint": "Corriente",
"dockgeURL": "URL de Dockge (ej. http://127.0.0.1:5001)",
"agentOnline": "Conectado",
"agentOffline": "Desconectado",
"connect": "Conectar",
"addAgent": "Añadir Agente",
"agentAddedSuccessfully": "Agente añadido satisfactoriamente.",
"removeAgent": "Remover Agente",
"removeAgentMsg": "Estás seguro que deseas remover este agente?",
"dockgeAgent": "Agentes Dockge",
"connecting": "Conectando",
"agentRemovedSuccessfully": "Agente removido satisfactoriamente."
} }

View File

@ -95,8 +95,21 @@
"connecting...": "Connexion au serveur socket…", "connecting...": "Connexion au serveur socket…",
"url": "URL | URLs", "url": "URL | URLs",
"extra": "Supplémentaire", "extra": "Supplémentaire",
"downStack": "Arrêter et désactiver", "downStack": "Arrêtez et rendre inactif",
"reverseProxyMsg1": "Utilisez vous un proxy inverse ?", "reverseProxyMsg1": "Utilisez vous un proxy inverse ?",
"Cannot connect to the socket server.": "Impossible de se connecter au serveur socket.", "Cannot connect to the socket server.": "Impossible de se connecter au serveur socket.",
"reconnecting...": "Reconnexion…" "reconnecting...": "Reconnexion…",
"newUpdate": "Nouvelle mise à jour",
"dockgeURL": "URL de Dockge (e.g. http://127.0.0.1:5001)",
"agentOnline": "En ligne",
"agentOffline": "Hors ligne",
"connecting": "Connexion",
"addAgent": "Ajouter un agent",
"agentAddedSuccessfully": "Agent ajouté avec succès.",
"agentRemovedSuccessfully": "Agent supprimé avec succès.",
"removeAgent": "Supprimer l'agent",
"dockgeAgent": "Dockge Agent | Dockge Agents",
"currentEndpoint": "Actuel",
"connect": "Connecter",
"removeAgentMsg": "Êtes-vous sûr de vouloir supprimer cet agent ?"
} }

115
frontend/src/lang/id.json Normal file
View File

@ -0,0 +1,115 @@
{
"Create your admin account": "Buat akun admin Anda",
"PasswordsDoNotMatch": "Kata sandi tidak sama.",
"Repeat Password": "Ulangi Kata Sandi",
"Create": "Buat",
"signedInDisp": "Masuk sebagai {0}",
"signedInDispDisabled": "Otentikasi Dinonaktifkan.",
"home": "Beranda",
"console": "Konsol",
"registry": "Registri",
"compose": "Menyusun",
"addFirstStackMsg": "Buat tumpukan pertama Anda!",
"stackName": "Nama Tumpukan",
"deployStack": "Terapkan",
"stopStack": "Hentikan",
"restartStack": "Mulai ulang",
"updateStack": "Pembaruan",
"downStack": "Hentikan & Turun",
"editStack": "Sunting",
"discardStack": "Buang",
"saveStackDraft": "Simpan",
"notAvailableShort": "T/A",
"stackNotManagedByDockgeMsg": "Tumpukan ini tidak dikelola oleh Dockge.",
"primaryHostname": "Nama Host Utama",
"general": "Umum",
"container": "Kontainer | Wadah",
"scanFolder": "Pindai Folder Tumpukan",
"restartPolicyUnlessStopped": "Kecuali Dihentikan",
"restartPolicyAlways": "Selalu",
"restartPolicyNo": "Tidak",
"environmentVariable": "Variabel Lingkungan | Variabel Lingkungan",
"dockerImage": "Image",
"startStack": "Mulai",
"restartPolicy": "Kebijakan Mulai Ulang",
"containerName": "Nama Kontainer",
"network": "Jaringan",
"dependsOn": "Ketergantungan Kontainer",
"addListItem": "Tambah {0}",
"deleteContainer": "Hapus",
"addContainer": "Tambah Kontainer",
"addNetwork": "Tambah Jaringan",
"disableauth.message1": "Apakah Anda yakin untuk <strong>mematikan otentikasi</strong>?",
"passwordNotMatchMsg": "Kata sandi berulang tidak cocok.",
"autoGet": "Otomatis Dapatkan",
"add": "Tambah",
"Edit": "Sunting",
"port": "Port",
"volume": "Volume",
"createExternalNetwork": "Buat",
"addInternalNetwork": "Tambah",
"Save": "Simpan",
"Language": "Bahasa",
"Change Password": "Ubah Kata Sandi",
"Current Password": "Ubah Kata Sandi",
"New Password": "Kata Sandi Baru",
"Repeat New Password": "Ulangi Kata Sandi",
"Update Password": "Perbarui Kata Sandi",
"Advanced": "Lanjutan",
"Enable Auth": "Hidupkan Otentikasi",
"Disable Auth": "Matikan Otentikasi",
"I understand, please disable": "Saya mengerti, tolong nonaktifkan",
"Leave": "Pergi",
"Frontend Version": "Versi Antarmuka",
"Check Update On GitHub": "Cek pembaruan di Github",
"Show update if available": "Tampilkan pembaruan jika tersedia",
"Remember me": "Ingat saya",
"Login": "Masuk",
"Username": "Nama Pengguna",
"Password": "Kata Sandi",
"Settings": "Pengaturan",
"Logout": "Keluar",
"Lowercase only": "Huruf kecil saja",
"Convert to Compose": "Ubah ke Tumpukan",
"active": "aktif",
"exited": "keluar",
"inactive": "nonaktif",
"Appearance": "Tampilan",
"Security": "Keamanan",
"About": "Tentang",
"Internal Networks": "Jaringan internal",
"External Networks": "Jaringan eksternal",
"No External Networks": "Tanpa Jaringan Eksternal",
"reverseProxyMsg1": "Menggunakan Reverse Proxy ?",
"Cannot connect to the socket server.": "Tidak bisa terhubung dengan server socket.",
"reconnecting...": "Menghubungkan kembali…",
"connecting...": "Menyambungkan ke server socket…",
"url": "TAUTAN",
"extra": "Ekstra",
"Docker Run": "Jalankan Docker",
"newUpdate": "Pembaruan Baru",
"languageName": "Bahasa Indonesia (Indonesian)",
"authIncorrectCreds": "Nama pengguna atau sandi salah.",
"deleteStack": "Hapus",
"deleteStackMsg": "Apakah Anda yakin Anda ingin menghapus tumpukan ini ?",
"restartPolicyOnFailure": "Ketika Gagal",
"disableauth.message2": "Ini dirancang untuk skenario <strong>di mana Anda bermaksud untuk mengimplementasikan otentikasi pihak ketiga</strong> di depan Dockge seperti Cloudflare Access, Authelia, atau mekanisme otentikasi lainnya.",
"applyToYAML": "Aplikasikan ke YAML",
"Current User": "Pengguna Saat Ini",
"Please use this option carefully!": "Mohon berhati - hati menggunakan opsi ini!",
"Also check beta release": "Juga cek keluaran beta",
"Allowed commands:": "Perintah yang diperbolehkan:",
"reverseProxyMsg2": "Lihat cara mengonfigurasinya untuk WebSocket",
"dockgeURL": "Alamat Dockge (cth. http://127.0.0.1:5001)",
"connecting": "Menghubungkan",
"addAgent": "Tambah Agen",
"agentAddedSuccessfully": "Agen sukses ditambahkan.",
"agentRemovedSuccessfully": "Agen sukses dihapus.",
"removeAgent": "Hapus Agen",
"connect": "Hubungkan",
"dockgeAgent": "Agen Dockge",
"currentEndpoint": "Sekarang",
"agentOnline": "Terhubung",
"agentOffline": "Terputus",
"removeAgentMsg": "Apakah anda yakin untuk menghapus agen ini?"
}

View File

@ -10,10 +10,10 @@
"home": "Home", "home": "Home",
"console": "Console", "console": "Console",
"registry": "Registro", "registry": "Registro",
"compose": "Compose", "compose": "Componi",
"addFirstStackMsg": "Componi il tuo primo stack!", "addFirstStackMsg": "Componi il tuo primo stack!",
"stackName": "Nome dello stack", "stackName": "Nome dello stack",
"deployStack": "Deploy", "deployStack": "Rilascia",
"deleteStack": "Cancella", "deleteStack": "Cancella",
"stopStack": "Stop", "stopStack": "Stop",
"restartStack": "Riavvia", "restartStack": "Riavvia",
@ -75,7 +75,7 @@
"Also check beta release": "Controlla anche le release in beta", "Also check beta release": "Controlla anche le release in beta",
"Remember me": "Ricordami", "Remember me": "Ricordami",
"Login": "Login", "Login": "Login",
"Username": "Username", "Username": "Nome Utente",
"Password": "Password", "Password": "Password",
"Settings": "Impostazioni", "Settings": "Impostazioni",
"Logout": "Logout", "Logout": "Logout",
@ -97,5 +97,6 @@
"Cannot connect to the socket server.": "Impossibile connettersi al server socket.", "Cannot connect to the socket server.": "Impossibile connettersi al server socket.",
"connecting...": "Connessione al server socket…", "connecting...": "Connessione al server socket…",
"extra": "Extra", "extra": "Extra",
"reconnecting...": "Riconnessione…" "reconnecting...": "Riconnessione…",
"url": "Indirizzo | Indirizzi"
} }

View File

@ -34,7 +34,7 @@
"primaryHostname": "主ホスト名", "primaryHostname": "主ホスト名",
"container": "コンテナ", "container": "コンテナ",
"dependsOn": "コンテナ依存関係", "dependsOn": "コンテナ依存関係",
"downStack": "停止して削除", "downStack": "停止してInactive",
"notAvailableShort": "N/A", "notAvailableShort": "N/A",
"restartPolicyUnlessStopped": "手動で停止されるまで", "restartPolicyUnlessStopped": "手動で停止されるまで",
"restartPolicyAlways": "常時", "restartPolicyAlways": "常時",

View File

@ -92,11 +92,24 @@
"External Networks": "외부 네트워크", "External Networks": "외부 네트워크",
"No External Networks": "외부 네트워크 없음", "No External Networks": "외부 네트워크 없음",
"reverseProxyMsg2": "여기서 WebSocket을 위한 설정을 확인해 보세요", "reverseProxyMsg2": "여기서 WebSocket을 위한 설정을 확인해 보세요",
"downStack": "정지 & Down", "downStack": "정지 & 비활성화",
"reverseProxyMsg1": "리버스 프록시를 사용하고 계신가요?", "reverseProxyMsg1": "리버스 프록시를 사용하고 계신가요?",
"Cannot connect to the socket server.": "소켓 서버에 연결하지 못했습니다.", "Cannot connect to the socket server.": "소켓 서버에 연결하지 못했습니다.",
"connecting...": "소켓 서버에 연결하는 중…", "connecting...": "소켓 서버에 연결하는 중…",
"extra": "기타", "extra": "기타",
"url": "URL | URL", "url": "URL | URL",
"reconnecting...": "재연결 중…" "reconnecting...": "재연결 중…",
"newUpdate": "새 업데이트",
"dockgeURL": "Dockge URL (예. http://127.0.0.1:5001)",
"agentOnline": "온라인",
"agentOffline": "오프라인",
"connect": "연결",
"addAgent": "에이전트 추가",
"agentAddedSuccessfully": "에이전트를 성공적으로 추가했습니다.",
"removeAgent": "에이전트 삭제",
"removeAgentMsg": "정말로 이 에이전트를 삭제하시겠습니까?",
"dockgeAgent": "Dockge 에이전트",
"currentEndpoint": "현재",
"connecting": "연결 중",
"agentRemovedSuccessfully": "에이전트를 성공적으로 삭제했습니다."
} }

View File

@ -1,14 +1,14 @@
{ {
"languageName": "Nederlands", "languageName": "Nederlands",
"authIncorrectCreds": "Onjuiste gebruikersnaam of wachtwoord.", "authIncorrectCreds": "Onjuiste gebruikersnaam of wachtwoord.",
"PasswordsDoNotMatch": "Paswoorden komen niet overeen.", "PasswordsDoNotMatch": "Wachtwoorden komen niet overeen.",
"Repeat Password": "Herhaal wachtwoord", "Repeat Password": "Herhaal wachtwoord",
"Create": "Aanmaken", "Create": "Aanmaken",
"signedInDisp": "Ingelogd als {0}", "signedInDisp": "Ingelogd als {0}",
"home": "Startpagina", "home": "Home",
"console": "Console", "console": "Console",
"registry": "Register", "registry": "Register",
"compose": "Samenstellen", "compose": "Nieuwe stack",
"stackName": "Stack naam", "stackName": "Stack naam",
"deployStack": "Opzetten", "deployStack": "Opzetten",
"deleteStack": "Verwijder", "deleteStack": "Verwijder",
@ -16,11 +16,11 @@
"restartStack": "Herstart", "restartStack": "Herstart",
"updateStack": "Update", "updateStack": "Update",
"startStack": "Start", "startStack": "Start",
"downStack": "Stop & Down", "downStack": "Stop & Afsluiten",
"editStack": "Bewerken", "editStack": "Bewerken",
"discardStack": "Verwijderen", "discardStack": "Verwijderen",
"saveStackDraft": "Opslaan", "saveStackDraft": "Opslaan",
"notAvailableShort": "NVT", "notAvailableShort": "n.v.t.",
"stackNotManagedByDockgeMsg": "Deze stack wordt niet beheerd door Dockge.", "stackNotManagedByDockgeMsg": "Deze stack wordt niet beheerd door Dockge.",
"primaryHostname": "Primaire hostnaam", "primaryHostname": "Primaire hostnaam",
"general": "Algemeen", "general": "Algemeen",
@ -83,9 +83,9 @@
"reverseProxyMsg1": "Reverse proxy in gebruik?", "reverseProxyMsg1": "Reverse proxy in gebruik?",
"reverseProxyMsg2": "Controleer hoe te configureren voor WebSocket", "reverseProxyMsg2": "Controleer hoe te configureren voor WebSocket",
"Cannot connect to the socket server.": "Kan geen verbinding maken met de socket server.", "Cannot connect to the socket server.": "Kan geen verbinding maken met de socket server.",
"reconnecting...": "Herverbinden...", "reconnecting...": "Herverbinden",
"connecting...": "Verbinden met de socket server...", "connecting...": "Verbinden met de socket server",
"url": "Url(s)", "url": "Adres(sen)",
"extra": "Extra", "extra": "Extra",
"Create your admin account": "Creëer je beheerders-account", "Create your admin account": "Creëer je beheerders-account",
"addFirstStackMsg": "Maak je eerste stack!", "addFirstStackMsg": "Maak je eerste stack!",
@ -98,5 +98,18 @@
"Please use this option carefully!": "Wees voorzichtig met deze optie!", "Please use this option carefully!": "Wees voorzichtig met deze optie!",
"Also check beta release": "Controleer ook op beta releases", "Also check beta release": "Controleer ook op beta releases",
"Convert to Compose": "Converteer naar compose", "Convert to Compose": "Converteer naar compose",
"External Networks": "Externe netwerken" "External Networks": "Externe netwerken",
"newUpdate": "Nieuwe update",
"dockgeAgent": "Dockge Agent | Dockge Agents",
"currentEndpoint": "Huidige",
"dockgeURL": "Dockge Adres (bijv. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Verbinden",
"connect": "Verbind",
"addAgent": "Agent toevoegen",
"agentAddedSuccessfully": "Agent toegevoegd.",
"agentRemovedSuccessfully": "Agent verwijderd.",
"removeAgent": "Verwijder agent",
"removeAgentMsg": "Weet je zeker dat je deze agent wilt verwijderen?"
} }

View File

@ -92,11 +92,24 @@
"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", "reverseProxyMsg2": "Veja como configurar para WebSocket",
"downStack": "Parar & Encerrar", "downStack": "Parar & Inativar",
"reverseProxyMsg1": "Utiliza proxy reverso?", "reverseProxyMsg1": "Utiliza proxy reverso?",
"Cannot connect to the socket server.": "Não é possível conectar ao socket server.", "Cannot connect to the socket server.": "Não é possível conectar ao socket server.",
"connecting...": "Conectando ao socket server…", "connecting...": "Conectando ao socket server…",
"url": "URL | URLs", "url": "URL | URLs",
"extra": "Extra", "extra": "Extra",
"reconnecting...": "Reconectando…" "reconnecting...": "Reconectando…",
"newUpdate": "Nova Atualização",
"dockgeAgent": "Agente Dockge | Agentes Dockge",
"currentEndpoint": "Atual",
"dockgeURL": "Dockge URL (ex. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Conectando",
"connect": "Conectar",
"addAgent": "Adicionar agente",
"agentAddedSuccessfully": "Agente adicionado com sucesso.",
"agentRemovedSuccessfully": "Agente removido com sucesso.",
"removeAgent": "Remover Agente",
"removeAgentMsg": "Tem certeza de que deseja remover este agente?"
} }

View File

@ -12,7 +12,7 @@
"registry": "Registro", "registry": "Registro",
"compose": "Compor", "compose": "Compor",
"addFirstStackMsg": "Componha sua primeira pilha!", "addFirstStackMsg": "Componha sua primeira pilha!",
"stackName" : "Nome da Pilha", "stackName": "Nome da Pilha",
"deployStack": "Implantar", "deployStack": "Implantar",
"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 de que deseja excluir esta pilha?", "deleteStackMsg": "Tem certeza de que deseja excluir esta pilha?",
"stackNotManagedByDockgeMsg": "Esta pilha não é gerenciada pelo Dockge.", "stackNotManagedByDockgeMsg": "Esta pilha não é gerenciada pelo Dockge.",
"primaryHostname": "Nome do Host Primário", "primaryHostname": "Nome do Host Primário",
@ -90,5 +90,26 @@
"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",
"newUpdate": "Nova Atualização",
"currentEndpoint": "Atual",
"dockgeURL": "Dockge URL (e.g. http://127.0.0.1:5001)",
"agentOnline": "Online",
"agentOffline": "Offline",
"connecting": "Conectando",
"addAgent": "Adicionar Agente",
"agentAddedSuccessfully": "Agente adicionado com sucesso.",
"agentRemovedSuccessfully": "Agente removido com sucesso.",
"removeAgent": "Remover Agente",
"downStack": "Parar & Inativar",
"dockgeAgent": "Dockge Agente | Dockge Agentes",
"connect": "Conectar",
"removeAgentMsg": "Tem certeza de que deseja remover este agente?",
"reverseProxyMsg1": "Usando um Proxy Reverso?",
"reverseProxyMsg2": "Verifique para configurá-lo como WebSocket",
"Cannot connect to the socket server.": "Não é possível se conectar ao servidor socket.",
"url": "URL | URLs",
"extra": "Extra",
"reconnecting...": "Reconectando…",
"connecting...": "Conectando ao servidor de socket…"
} }

View File

@ -81,9 +81,9 @@
"Lowercase only": "Только нижний регистр", "Lowercase only": "Только нижний регистр",
"Convert to Compose": "Преобразовать в Compose", "Convert to Compose": "Преобразовать в Compose",
"Docker Run": "Запустить Docker", "Docker Run": "Запустить Docker",
"active": "активный", "active": "активные",
"exited": "завершенный", "exited": "остановленные",
"inactive": "неактинвый", "inactive": "неактивных",
"Appearance": "Внешний вид", "Appearance": "Внешний вид",
"Security": "Безопасность", "Security": "Безопасность",
"About": "О продукте", "About": "О продукте",
@ -91,12 +91,25 @@
"Internal Networks": "Внутренние сети", "Internal Networks": "Внутренние сети",
"External Networks": "Внешние сети", "External Networks": "Внешние сети",
"No External Networks": "Нет внешних сетей", "No External Networks": "Нет внешних сетей",
"downStack": "Остановить и выключить", "downStack": "Остановить и деактивировать",
"reverseProxyMsg1": "Использовать Реверс Прокси?", "reverseProxyMsg1": "Использовать Реверс Прокси?",
"reconnecting...": "Переподключение…", "reconnecting...": "Переподключение…",
"Cannot connect to the socket server.": "Не удается подключиться к серверу сокетов.", "Cannot connect to the socket server.": "Не удается подключиться к серверу сокетов.",
"url": "URL адрес(а)", "url": "URL адрес(а)",
"extra": "Дополнительно", "extra": "Дополнительно",
"reverseProxyMsg2": "Проверьте, как настроить его для WebSocket", "reverseProxyMsg2": "Проверьте, как настроить его для WebSocket",
"connecting...": "Подключение к серверу сокетов…" "connecting...": "Подключение к серверу сокетов…",
"newUpdate": "Доступно обновление",
"currentEndpoint": "Текущий",
"agentOnline": "В сети",
"agentOffline": "Не в сети",
"connecting": "Подключение",
"connect": "Подключен",
"addAgent": "Добавить Агента",
"agentAddedSuccessfully": "Агент добавлен успешно.",
"removeAgent": "Удалить Агента",
"removeAgentMsg": "Вы уверены, что хотите удалить этого агента?",
"dockgeAgent": "Dockge Агент | Dockge Агенты",
"dockgeURL": "Dockge URL (например http://127.0.0.1:5001)",
"agentRemovedSuccessfully": "Агент удален успешно."
} }

View File

@ -90,5 +90,14 @@
"Allowed commands:": "Dovoljeni ukazi:", "Allowed commands:": "Dovoljeni ukazi:",
"Internal Networks": "Notranja omrežja", "Internal Networks": "Notranja omrežja",
"External Networks": "Zunanja 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 …",
"newUpdate": "Nova posodobitev",
"reverseProxyMsg2": "Preverite, kako ga konfigurirati za WebSocket",
"Cannot connect to the socket server.": "Ni mogoče vzpostaviti povezave s strežnikom vtičnic.",
"url": "URL | URL-ji"
} }

View File

@ -19,7 +19,7 @@
"restartStack": "Starta om", "restartStack": "Starta om",
"updateStack": "Uppdatera", "updateStack": "Uppdatera",
"startStack": "Starta", "startStack": "Starta",
"downStack": "Stoppa & Ner", "downStack": "Stoppa & Inaktivera",
"editStack": "Redigera", "editStack": "Redigera",
"discardStack": "Kasta", "discardStack": "Kasta",
"saveStackDraft": "Spara", "saveStackDraft": "Spara",
@ -98,5 +98,18 @@
"reverseProxyMsg2": "Kontrollera hur man konfigurerar webbsocket", "reverseProxyMsg2": "Kontrollera hur man konfigurerar webbsocket",
"url": "URL | URLer", "url": "URL | URLer",
"extra": "Extra", "extra": "Extra",
"reconnecting...": "Återansluter…" "reconnecting...": "Återansluter…",
"newUpdate": "Ny uppdatering",
"currentEndpoint": "Nuvarande",
"dockgeURL": "Dockge URL (ex. http://127.0.0.1:5001)",
"agentOnline": "On-line",
"agentOffline": "Off-line",
"connecting": "Ansluter",
"connect": "Ansluten",
"addAgent": "Lägg till agent",
"agentRemovedSuccessfully": "Agent borttagen.",
"removeAgent": "Ta bort agent",
"removeAgentMsg": "Är du säker att du vill ta bort denna agent?",
"dockgeAgent": "Dockge agenter | Dockge agenter",
"agentAddedSuccessfully": "Agent tillagd."
} }

View File

@ -91,5 +91,12 @@
"Allowed commands:": "คำสั่งที่อนุญาต:", "Allowed commands:": "คำสั่งที่อนุญาต:",
"Internal Networks": "เครือข่ายภายใน", "Internal Networks": "เครือข่ายภายใน",
"External 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,11 +92,24 @@
"External Networks": "Зовнішні мережі", "External Networks": "Зовнішні мережі",
"No External Networks": "Немає зовнішніх мереж", "No External Networks": "Немає зовнішніх мереж",
"downStack": "Зупинити і вимкнути", "downStack": "Зупинити і вимкнути",
"reverseProxyMsg1": "Використовувати зворотній проксі?", "reverseProxyMsg1": "Використовуєте зворотній проксі?",
"Cannot connect to the socket server.": "Не вдається підключитися до сервера сокетів.", "Cannot connect to the socket server.": "Не вдається підключитися до сервера сокетів.",
"reconnecting...": "Повторне підключення…", "reconnecting...": "Повторне підключення…",
"connecting...": "Підключення до сервера сокетів…", "connecting...": "Підключення до сервера сокетів…",
"url": "URL-адреса | URL-адреси", "url": "URL-адреса | URL-адреси",
"reverseProxyMsg2": "Перевірте, як налаштувати його для WebSocket", "reverseProxyMsg2": "Перевірте, як налаштувати його для WebSocket",
"extra": "Додатково" "extra": "Додатково",
"newUpdate": "Оновлення",
"currentEndpoint": "Поточний",
"agentOnline": "Онлайн",
"agentOffline": "Офлайн",
"connecting": "Підключення",
"connect": "Підключитися",
"addAgent": "Додати агент",
"removeAgent": "Видалити агент",
"dockgeAgent": "Dockge-агент | Dockge-агенти",
"dockgeURL": "Dockge URL (напр. http://127.0.0.1:5001)",
"agentRemovedSuccessfully": "Агент успішно видалено.",
"agentAddedSuccessfully": "Агент успішно додано.",
"removeAgentMsg": "Ви впевнені, що хочете видалити цей агент?"
} }

115
frontend/src/lang/vi.json Normal file
View File

@ -0,0 +1,115 @@
{
"authIncorrectCreds": "Sai tên người dùng hoặc mật khẩu.",
"PasswordsDoNotMatch": "Mật khẩu không khớp.",
"Repeat Password": "Lặp Lại Mật Khẩu",
"Create": "Tạo",
"signedInDisp": "Đã đăng nhập với tư cách {0}",
"home": "Trang chủ",
"console": "Console",
"compose": "Compose",
"registry": "Registry",
"stackName": "Tên Stack",
"deployStack": "Triển khai",
"deleteStack": "Xoá",
"stopStack": "Dừng",
"restartStack": "Khởi động lại",
"signedInDispDisabled": "Đã Tắt Xác Thực Đăng Nhập.",
"startStack": "Bắt đầu",
"downStack": "Dừng & Ngưng hoạt động",
"editStack": "Chỉnh sửa",
"saveStackDraft": "Lưu",
"notAvailableShort": "N/A",
"deleteStackMsg": "Bạn có chắc chắn muốn xoá stack này?",
"primaryHostname": "Tên Host Chính",
"scanFolder": "Quét Thư Mục Stack",
"restartPolicyAlways": "Luôn Luôn",
"restartPolicyOnFailure": "Khi Có Lỗi",
"restartPolicyNo": "Không",
"environmentVariable": "Biến Môi Trường | Các Biến Môi Trường",
"restartPolicy": "Chính Sách Khởi Động Lại",
"containerName": "Tên Container",
"port": "Cổng | Cổng",
"addListItem": "Thêm {0}",
"deleteContainer": "Xoá",
"addContainer": "Thêm Container",
"addNetwork": "Thêm Mạng",
"passwordNotMatchMsg": "Mật khẩu nhập lại không khớp.",
"autoGet": "Tự Động Lấy",
"add": "Thêm",
"Edit": "Chỉnh sửa",
"applyToYAML": "Áp dụng cho YAML",
"createExternalNetwork": "Tạo",
"addInternalNetwork": "Thêm",
"Save": "Lưu",
"Language": "Ngôn ngữ",
"Current User": "Người Dùng Hiện Tại",
"Change Password": "Đổi Mật Khẩu",
"Current Password": "Mật Khẩu Hiện Tại",
"New Password": "Mật Khẩu Mới",
"Repeat New Password": "Nhập Lại Mật Khẩu Mới",
"Update Password": "Cập Nhật Mật Khẩu",
"Advanced": "Nâng cao",
"Please use this option carefully!": "Vui lòng sử dụng tuỳ chọn này cẩn thận!",
"Enable Auth": "Kích Hoạt Xác Thực Đăng Nhập",
"Disable Auth": "Vô Hiệu Xác Thực Đăng Nhập",
"I understand, please disable": "Tôi hiểu, vui lòng vô hiệu",
"Leave": "Rời",
"Frontend Version": "Phiên Bản Giao Diện Người Dùng",
"Check Update On GitHub": "Kiểm Tra Cập Nhật Trên Github",
"Also check beta release": "Kiểm tra cả bản phát hành beta",
"Remember me": "Ghi nhớ tôi",
"Login": "Đăng nhập",
"Username": "Tên người dùng",
"Password": "Mật khẩu",
"Settings": "Cài đặt",
"Logout": "Đăng xuất",
"Lowercase only": "Chỉ viết thường",
"Convert to Compose": "Chuyển đổi sang Compose",
"Docker Run": "Chạy Docker",
"active": "hoạt động",
"exited": "đã thoát",
"inactive": "không hoạt động",
"Security": "Bảo Mật",
"Appearance": "Giao Diện",
"About": "Về",
"Allowed commands:": "Các lệnh được cho phép:",
"Internal Networks": "Mạng Nội Bộ",
"External Networks": "Mạng Ngoại Vi",
"No External Networks": "Không Có Mạng Ngoại Vi",
"reverseProxyMsg1": "Đang sử dụng Reverse Proxy?",
"reverseProxyMsg2": "Xem cách để cấu hình nó cho WebSocket",
"Cannot connect to the socket server.": "Không thể kết nối tới máy chủ socket.",
"reconnecting...": "Đang kết nối lại…",
"connecting...": "Đang kết nối tới máy chủ socket…",
"url": "URL",
"extra": "Bổ sung",
"newUpdate": "Cập Nhật Mới",
"dockgeAgent": "Dockge Agent",
"currentEndpoint": "Đang sử dụng",
"dockgeURL": "URL của Dockge (v.d. http://127.0.0.1:5001)",
"agentOnline": "Trực tuyến",
"agentOffline": "Ngoại tuyến",
"connecting": "Đang kết nối",
"connect": "Kết nối",
"addAgent": "Thêm Agent",
"agentAddedSuccessfully": "Agent đã được thêm thành công.",
"agentRemovedSuccessfully": "Agent đã được xoá thành công.",
"removeAgent": "Xoá Agent",
"removeAgentMsg": "Bạn có chắc chắn muốn xoá agent này?",
"languageName": "Tiếng Việt",
"Create your admin account": "Tạo tài khoản admin của bạn",
"addFirstStackMsg": "Tạo stack đầu tiên của bạn!",
"volume": "Volume | Volume",
"updateStack": "Cập nhật",
"network": "Mạng | Mạng",
"discardStack": "Loại bỏ",
"stackNotManagedByDockgeMsg": "Stack này không được quản lý bởi Dockge.",
"dependsOn": "Container Phụ Thuộc | Các Container Phụ Thuộc",
"general": "Tổng Quan",
"disableauth.message1": "Bạn có chắc chắn muốn <strong>tắt xác thực đăng nhập</strong>?",
"container": "Container",
"disableauth.message2": "Nó được thiết kế trong hoàn cảnh <strong>mà bạn dự định triển khai xác thực đăng nhập bên thứ ba</strong> trước Dockge như là Cloudflare Access, Authelia hay các phương thức xác minh đăng nhập khác.",
"dockerImage": "Image",
"Show update if available": "Hiển thị cập nhật nếu có",
"restartPolicyUnlessStopped": "Trừ Khi Dừng Lại"
}

View File

@ -98,5 +98,5 @@
"Cannot connect to the socket server.": "无法连接到socket服务器。", "Cannot connect to the socket server.": "无法连接到socket服务器。",
"url": "网址 | 网址", "url": "网址 | 网址",
"extra": "额外", "extra": "额外",
"downStack": "停止并删除" "downStack": "停止并置于非活动状态"
} }

View File

@ -1,5 +1,5 @@
{ {
"languageName": "繁體中文(台灣)", "languageName": "繁體中文 (台灣)",
"Create your admin account": "建立您的管理員帳號", "Create your admin account": "建立您的管理員帳號",
"authIncorrectCreds": "使用者名稱或密碼錯誤。", "authIncorrectCreds": "使用者名稱或密碼錯誤。",
"PasswordsDoNotMatch": "兩次輸入的密碼不一致。", "PasswordsDoNotMatch": "兩次輸入的密碼不一致。",
@ -51,7 +51,7 @@
"autoGet": "自動取得", "autoGet": "自動取得",
"add": "新增", "add": "新增",
"Edit": "編輯", "Edit": "編輯",
"applyToYAML": "應用到YAML", "applyToYAML": "套用到 YAML",
"createExternalNetwork": "建立", "createExternalNetwork": "建立",
"addInternalNetwork": "新增", "addInternalNetwork": "新增",
"Save": "儲存", "Save": "儲存",
@ -71,7 +71,7 @@
"Frontend Version": "前端版本", "Frontend Version": "前端版本",
"Check Update On GitHub": "在 GitHub 上檢查更新", "Check Update On GitHub": "在 GitHub 上檢查更新",
"Show update if available": "有更新時提醒我", "Show update if available": "有更新時提醒我",
"Also check beta release": "同時檢查 Beta 渠道更新", "Also check beta release": "同時檢查 Beta 更新",
"Remember me": "記住我", "Remember me": "記住我",
"Login": "登入", "Login": "登入",
"Username": "使用者名稱", "Username": "使用者名稱",
@ -91,12 +91,14 @@
"Internal Networks": "內部網路", "Internal Networks": "內部網路",
"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...": "重新連線中…",
"connecting...": "連線至 Socket 伺服器中…", "connecting...": "連線至 Socket 伺服器中…",
"url": "網址 | 網址", "url": "網址 | 網址",
"extra": "額外" "extra": "額外",
"newUpdate": "新版本",
"currentEndpoint": "目前"
} }

View File

@ -98,6 +98,7 @@
<script> <script>
import Login from "../components/Login.vue"; import Login from "../components/Login.vue";
import { compareVersions } from "compare-versions"; import { compareVersions } from "compare-versions";
import { ALL_ENDPOINTS } from "../../../common/util-common";
export default { export default {
@ -145,7 +146,7 @@ export default {
methods: { methods: {
scanFolder() { scanFolder() {
this.$root.getSocket().emit("requestStackList", (res) => { this.$root.emitAgent(ALL_ENDPOINTS, "requestStackList", (res) => {
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
}, },

View File

@ -1,5 +1,5 @@
// Dayjs init inside this, so it has to be the first import // Dayjs init inside this, so it has to be the first import
import "../../backend/util-common"; import "../../common/util-common";
import { createApp, defineComponent, h } from "vue"; import { createApp, defineComponent, h } from "vue";
import App from "./App.vue"; import App from "./App.vue";
@ -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

View File

@ -2,7 +2,8 @@ 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";
import { AgentSocket } from "../../../common/agent-socket";
let socket : Socket; let socket : Socket;
@ -28,16 +29,51 @@ export default defineComponent({
loggedIn: false, loggedIn: false,
allowLoginDialog: false, allowLoginDialog: false,
username: null, username: null,
stackList: {},
composeTemplate: "", composeTemplate: "",
stackList: {},
// All stack list from all agents
allAgentStackList: {} as Record<string, object>,
// online / offline / connecting
agentStatusList: {
},
// Agent List
agentList: {
},
}; };
}, },
computed: { computed: {
agentCount() {
return Object.keys(this.agentList).length;
},
completeStackList() {
let list : Record<string, object> = {};
for (let stackName in this.stackList) {
list[stackName + "_"] = this.stackList[stackName];
}
for (let endpoint in this.allAgentStackList) {
let instance = this.allAgentStackList[endpoint];
for (let stackName in instance.stackList) {
list[stackName + "_" + endpoint] = instance.stackList[stackName];
}
}
return list;
},
usernameFirstChar() { usernameFirstChar() {
if (typeof this.username == "string" && this.username.length >= 1) { if (typeof this.username == "string" && this.username.length >= 1) {
return this.username.charAt(0).toUpperCase(); return this.username.charAt(0).toUpperCase();
} else { } else {
return "🐻"; return "🐬";
} }
}, },
@ -65,6 +101,15 @@ export default defineComponent({
}, },
watch: { watch: {
"socketIO.connected"() {
if (this.socketIO.connected) {
this.agentStatusList[""] = "online";
} else {
this.agentStatusList[""] = "offline";
}
},
remember() { remember() {
localStorage.remember = (this.remember) ? "1" : "0"; localStorage.remember = (this.remember) ? "1" : "0";
}, },
@ -84,6 +129,15 @@ export default defineComponent({
}, },
methods: { methods: {
endpointDisplayFunction(endpoint : string) {
if (endpoint) {
return endpoint;
} else {
return this.$t("currentEndpoint");
}
},
/** /**
* Initialize connection to socket server * Initialize connection to socket server
* @param bypass Should the check for if we * @param bypass Should the check for if we
@ -108,8 +162,12 @@ export default defineComponent({
this.socketIO.connecting = true; this.socketIO.connecting = true;
}, 1500); }, 1500);
socket = io(url, { socket = io(url);
transports: [ "websocket", "polling" ]
// Handling events from agents
let agentSocket = new AgentSocket();
socket.on("agent", (eventName : unknown, ...args : unknown[]) => {
agentSocket.call(eventName, ...args);
}); });
socket.on("connect", () => { socket.on("connect", () => {
@ -177,7 +235,7 @@ export default defineComponent({
this.$router.push("/setup"); this.$router.push("/setup");
}); });
socket.on("terminalWrite", (terminalName, data) => { agentSocket.on("terminalWrite", (terminalName, data) => {
const terminal = terminalMap.get(terminalName); const terminal = terminalMap.get(terminalName);
if (!terminal) { if (!terminal) {
//console.error("Terminal not found: " + terminalName); //console.error("Terminal not found: " + terminalName);
@ -186,9 +244,18 @@ export default defineComponent({
terminal.write(data); terminal.write(data);
}); });
socket.on("stackList", (res) => { agentSocket.on("stackList", (res) => {
if (res.ok) { if (res.ok) {
this.stackList = res.stackList; if (!res.endpoint) {
this.stackList = res.stackList;
} else {
if (!this.allAgentStackList[res.endpoint]) {
this.allAgentStackList[res.endpoint] = {
stackList: {},
};
}
this.allAgentStackList[res.endpoint].stackList = res.stackList;
}
} }
}); });
@ -203,6 +270,21 @@ export default defineComponent({
} }
}); });
socket.on("agentStatus", (res) => {
this.agentStatusList[res.endpoint] = res.status;
if (res.msg) {
this.toastError(res.msg);
}
});
socket.on("agentList", (res) => {
console.log(res);
if (res.ok) {
this.agentList = res.agentList;
}
});
socket.on("refresh", () => { socket.on("refresh", () => {
location.reload(); location.reload();
}); });
@ -220,6 +302,10 @@ export default defineComponent({
return socket; return socket;
}, },
emitAgent(endpoint : string, eventName : string, ...args : unknown[]) {
this.getSocket().emit("agent", endpoint, eventName, ...args);
},
/** /**
* Get payload of JWT cookie * Get payload of JWT cookie
* @returns {(object | undefined)} JWT payload * @returns {(object | undefined)} JWT payload
@ -310,9 +396,9 @@ export default defineComponent({
}, },
bindTerminal(terminalName : string, terminal : Terminal) { bindTerminal(endpoint : string, terminalName : string, terminal : Terminal) {
// Load terminal, get terminal screen // Load terminal, get terminal screen
socket.emit("terminalJoin", terminalName, (res) => { this.emitAgent(endpoint, "terminalJoin", terminalName, (res) => {
if (res.ok) { if (res.ok) {
terminal.write(res.buffer); terminal.write(res.buffer);
terminalMap.set(terminalName, terminal); terminalMap.set(terminalName, terminal);

View File

@ -2,7 +2,12 @@
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div> <div>
<h1 v-if="isAdd" class="mb-3">Compose</h1> <h1 v-if="isAdd" class="mb-3">Compose</h1>
<h1 v-else class="mb-3"><Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}</h1> <h1 v-else class="mb-3">
<Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}
<span v-if="$root.agentCount > 1" class="agent-name">
({{ endpointDisplay }})
</span>
</h1>
<div v-if="stack.isManagedByDockge" class="mb-3"> <div v-if="stack.isManagedByDockge" class="mb-3">
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
@ -70,6 +75,7 @@
ref="progressTerminal" ref="progressTerminal"
class="mb-3 terminal" class="mb-3 terminal"
:name="terminalName" :name="terminalName"
:endpoint="endpoint"
:rows="progressTerminalRows" :rows="progressTerminalRows"
@has-data="showProgressTerminal = true; submitted = true;" @has-data="showProgressTerminal = true; submitted = true;"
></Terminal> ></Terminal>
@ -87,6 +93,16 @@
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase"> <input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
<div class="form-text">{{ $t("Lowercase only") }}</div> <div class="form-text">{{ $t("Lowercase only") }}</div>
</div> </div>
<!-- Endpoint -->
<div class="mt-3">
<label for="name" class="form-label">{{ $t("dockgeAgent") }}</label>
<select v-model="stack.endpoint" class="form-select">
<option v-for="(agent, endpoint) in $root.agentList" :key="endpoint" :value="endpoint" :disabled="$root.agentStatusList[endpoint] != 'online'">
({{ $root.agentStatusList[endpoint] }}) {{ (endpoint) ? endpoint : $t("currentEndpoint") }}
</option>
</select>
</div>
</div> </div>
</div> </div>
@ -139,6 +155,7 @@
ref="combinedTerminal" ref="combinedTerminal"
class="mb-3 terminal" class="mb-3 terminal"
:name="combinedTerminalName" :name="combinedTerminalName"
:endpoint="endpoint"
:rows="combinedTerminalRows" :rows="combinedTerminalRows"
:cols="combinedTerminalCols" :cols="combinedTerminalCols"
style="height: 350px;" style="height: 350px;"
@ -236,7 +253,7 @@ import {
getComposeTerminalName, getComposeTerminalName,
PROGRESS_TERMINAL_ROWS, PROGRESS_TERMINAL_ROWS,
RUNNING RUNNING
} from "../../../backend/util-common"; } from "../../../common/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"; import dotenv from "dotenv";
@ -298,6 +315,10 @@ export default {
}, },
computed: { computed: {
endpointDisplay() {
return this.$root.endpointDisplayFunction(this.endpoint);
},
urls() { urls() {
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) { if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
return []; return [];
@ -334,7 +355,7 @@ export default {
* @return {*} * @return {*}
*/ */
globalStack() { globalStack() {
return this.$root.stackList[this.stack.name]; return this.$root.completeStackList[this.stack.name + "_" + this.endpoint];
}, },
status() { status() {
@ -349,20 +370,31 @@ export default {
if (!this.stack.name) { if (!this.stack.name) {
return ""; return "";
} }
return getComposeTerminalName(this.stack.name); return getComposeTerminalName(this.endpoint, this.stack.name);
}, },
combinedTerminalName() { combinedTerminalName() {
if (!this.stack.name) { if (!this.stack.name) {
return ""; return "";
} }
return getCombinedTerminalName(this.stack.name); return getCombinedTerminalName(this.endpoint, this.stack.name);
}, },
networks() { networks() {
return this.jsonConfig.networks; return this.jsonConfig.networks;
} },
endpoint() {
return this.stack.endpoint || this.$route.params.endpoint || "";
},
url() {
if (this.stack.endpoint) {
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
} else {
return `/compose/${this.stack.name}`;
}
},
}, },
watch: { watch: {
"stack.composeYAML": { "stack.composeYAML": {
@ -405,9 +437,7 @@ export default {
}, },
$route(to, from) { $route(to, from) {
// Leave Combined Terminal
console.debug("leaveCombinedTerminal", from.params.stackName);
this.$root.getSocket().emit("leaveCombinedTerminal", this.stack.name, () => {});
} }
}, },
mounted() { mounted() {
@ -437,6 +467,7 @@ export default {
composeYAML, composeYAML,
composeENV, composeENV,
isManagedByDockge: true, isManagedByDockge: true,
endpoint: "",
}; };
this.yamlCodeChange(); this.yamlCodeChange();
@ -449,11 +480,9 @@ export default {
this.requestServiceStatus(); this.requestServiceStatus();
}, },
unmounted() { unmounted() {
this.stopServiceStatusTimeout = true;
clearTimeout(serviceStatusTimeout);
}, },
methods: { methods: {
startServiceStatusTimeout() { startServiceStatusTimeout() {
clearTimeout(serviceStatusTimeout); clearTimeout(serviceStatusTimeout);
serviceStatusTimeout = setTimeout(async () => { serviceStatusTimeout = setTimeout(async () => {
@ -462,7 +491,7 @@ export default {
}, },
requestServiceStatus() { requestServiceStatus() {
this.$root.getSocket().emit("serviceStatusList", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "serviceStatusList", this.stack.name, (res) => {
if (res.ok) { if (res.ok) {
this.serviceStatusList = res.serviceStatusList; this.serviceStatusList = res.serviceStatusList;
} }
@ -475,22 +504,34 @@ export default {
exitConfirm(next) { exitConfirm(next) {
if (this.isEditMode) { if (this.isEditMode) {
if (confirm("You are currently editing a stack. Are you sure you want to leave?")) { if (confirm("You are currently editing a stack. Are you sure you want to leave?")) {
this.exitAction();
next(); next();
} else { } else {
next(false); next(false);
} }
} else { } else {
this.exitAction();
next(); next();
} }
}, },
exitAction() {
console.log("exitAction");
this.stopServiceStatusTimeout = true;
clearTimeout(serviceStatusTimeout);
// Leave Combined Terminal
console.debug("leaveCombinedTerminal", this.endpoint, this.stack.name);
this.$root.emitAgent(this.endpoint, "leaveCombinedTerminal", this.stack.name, () => {});
},
bindTerminal() { bindTerminal() {
this.$refs.progressTerminal?.bind(this.terminalName); this.$refs.progressTerminal?.bind(this.endpoint, this.terminalName);
}, },
loadStack() { loadStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("getStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "getStack", this.stack.name, (res) => {
if (res.ok) { if (res.ok) {
this.stack = res.stack; this.stack = res.stack;
this.yamlCodeChange(); this.yamlCodeChange();
@ -532,15 +573,15 @@ export default {
} }
} }
this.bindTerminal(this.terminalName); this.bindTerminal();
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => { this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
if (res.ok) { if (res.ok) {
this.isEditMode = false; this.isEditMode = false;
this.$router.push("/compose/" + this.stack.name); this.$router.push(this.url);
} }
}); });
}, },
@ -548,13 +589,13 @@ export default {
saveStack() { saveStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => { this.$root.emitAgent(this.stack.endpoint, "saveStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
if (res.ok) { if (res.ok) {
this.isEditMode = false; this.isEditMode = false;
this.$router.push("/compose/" + this.stack.name); this.$router.push(this.url);
} }
}); });
}, },
@ -562,7 +603,7 @@ export default {
startStack() { startStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("startStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "startStack", this.stack.name, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
@ -571,7 +612,7 @@ export default {
stopStack() { stopStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("stopStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "stopStack", this.stack.name, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
@ -580,7 +621,7 @@ export default {
downStack() { downStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("downStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "downStack", this.stack.name, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
@ -589,7 +630,7 @@ export default {
restartStack() { restartStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("restartStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "restartStack", this.stack.name, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
@ -598,14 +639,14 @@ export default {
updateStack() { updateStack() {
this.processing = true; this.processing = true;
this.$root.getSocket().emit("updateStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "updateStack", this.stack.name, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res); this.$root.toastRes(res);
}); });
}, },
deleteDialog() { deleteDialog() {
this.$root.getSocket().emit("deleteStack", this.stack.name, (res) => { this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
this.$root.toastRes(res); this.$root.toastRes(res);
if (res.ok) { if (res.ok) {
this.$router.push("/"); this.$router.push("/");
@ -750,6 +791,8 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../styles/vars.scss";
.terminal { .terminal {
height: 200px; height: 200px;
} }
@ -761,4 +804,9 @@ export default {
background-color: #2c2f38 !important; background-color: #2c2f38 !important;
} }
} }
.agent-name {
font-size: 13px;
color: $dark-font-color3;
}
</style> </style>

View File

@ -15,14 +15,14 @@
</p> </p>
</div> </div>
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console"></Terminal> <Terminal class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
import { allowedCommandList } from "../../../backend/util-common"; import { allowedCommandList } from "../../../common/util-common";
export default { export default {
components: { components: {
@ -32,6 +32,11 @@ export default {
allowedCommandList, allowedCommandList,
}; };
}, },
computed: {
endpoint() {
return this.$route.params.endpoint || "";
},
},
mounted() { mounted() {
}, },

View File

@ -7,13 +7,13 @@
<router-link :to="sh" class="btn btn-normal me-2">Switch to sh</router-link> <router-link :to="sh" class="btn btn-normal me-2">Switch to sh</router-link>
</div> </div>
<Terminal class="terminal" :rows="20" mode="interactive" :name="terminalName" :stack-name="stackName" :service-name="serviceName" :shell="shell"></Terminal> <Terminal class="terminal" :rows="20" mode="interactive" :name="terminalName" :stack-name="stackName" :service-name="serviceName" :shell="shell" :endpoint="endpoint"></Terminal>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
import { getContainerExecTerminalName } from "../../../backend/util-common"; import { getContainerExecTerminalName } from "../../../common/util-common";
export default { export default {
components: { components: {
@ -27,6 +27,9 @@ export default {
stackName() { stackName() {
return this.$route.params.stackName; return this.$route.params.stackName;
}, },
endpoint() {
return this.$route.params.endpoint || "";
},
shell() { shell() {
return this.$route.params.type; return this.$route.params.type;
}, },
@ -34,10 +37,12 @@ export default {
return this.$route.params.serviceName; return this.$route.params.serviceName;
}, },
terminalName() { terminalName() {
return getContainerExecTerminalName(this.stackName, this.serviceName, 0); return getContainerExecTerminalName(this.endpoint, this.stackName, this.serviceName, 0);
}, },
sh() { sh() {
return { let endpoint = this.$route.params.endpoint;
let data = {
name: "containerTerminal", name: "containerTerminal",
params: { params: {
stackName: this.stackName, stackName: this.stackName,
@ -45,6 +50,13 @@ export default {
type: "sh", type: "sh",
}, },
}; };
if (endpoint) {
data.name = "containerTerminalEndpoint";
data.params.endpoint = endpoint;
}
return data;
}, },
}, },
mounted() { mounted() {

View File

@ -5,36 +5,97 @@
{{ $t("home") }} {{ $t("home") }}
</h1> </h1>
<div class="shadow-box big-padding text-center mb-4"> <div class="row first-row">
<div class="row"> <!-- Left -->
<div class="col"> <div class="col-md-7">
<h3>{{ $t("active") }}</h3> <!-- Stats -->
<span class="num active">{{ activeNum }}</span> <div class="shadow-box big-padding text-center mb-4">
<div class="row">
<div class="col">
<h3>{{ $t("active") }}</h3>
<span class="num active">{{ activeNum }}</span>
</div>
<div class="col">
<h3>{{ $t("exited") }}</h3>
<span class="num exited">{{ exitedNum }}</span>
</div>
<div class="col">
<h3>{{ $t("inactive") }}</h3>
<span class="num inactive">{{ inactiveNum }}</span>
</div>
</div>
</div> </div>
<div class="col">
<h3>{{ $t("exited") }}</h3> <!-- Docker Run -->
<span class="num exited">{{ exitedNum }}</span> <h2 class="mb-3">{{ $t("Docker Run") }}</h2>
<div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</div> </div>
<div class="col">
<h3>{{ $t("inactive") }}</h3> <button class="btn-normal btn mb-4" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
<span class="num inactive">{{ inactiveNum }}</span> </div>
<!-- Right -->
<div class="col-md-5">
<!-- Agent List -->
<div class="shadow-box big-padding">
<h4 class="mb-3">{{ $tc("dockgeAgent", 2) }} <span class="badge bg-warning" style="font-size: 12px;">beta</span></h4>
<div v-for="(agent, endpoint) in $root.agentList" :key="endpoint" class="mb-3 agent">
<!-- Agent Status -->
<template v-if="$root.agentStatusList[endpoint]">
<span v-if="$root.agentStatusList[endpoint] === 'online'" class="badge bg-primary me-2">{{ $t("agentOnline") }}</span>
<span v-else-if="$root.agentStatusList[endpoint] === 'offline'" class="badge bg-danger me-2">{{ $t("agentOffline") }}</span>
<span v-else class="badge bg-secondary me-2">{{ $t($root.agentStatusList[endpoint]) }}</span>
</template>
<!-- Agent Display Name -->
<span v-if="endpoint === ''">{{ $t("currentEndpoint") }}</span>
<a v-else :href="agent.url" target="_blank">{{ endpoint }}</a>
<!-- Remove Button -->
<font-awesome-icon v-if="endpoint !== ''" class="ms-2 remove-agent" icon="trash" @click="showRemoveAgentDialog[agent.url] = !showRemoveAgentDialog[agent.url]" />
<!-- Remoe Agent Dialog -->
<BModal v-model="showRemoveAgentDialog[agent.url]" :okTitle="$t('removeAgent')" okVariant="danger" @ok="removeAgent(agent.url)">
<p>{{ agent.url }}</p>
{{ $t("removeAgentMsg") }}
</BModal>
</div>
<button v-if="!showAgentForm" class="btn btn-normal" @click="showAgentForm = !showAgentForm">{{ $t("addAgent") }}</button>
<!-- Add Agent Form -->
<form v-if="showAgentForm" @submit.prevent="addAgent">
<div class="mb-3">
<label for="url" class="form-label">{{ $t("dockgeURL") }}</label>
<input id="url" v-model="agent.url" type="url" class="form-control" required placeholder="http://">
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ $t("Username") }}</label>
<input id="username" v-model="agent.username" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label>
<input id="password" v-model="agent.password" type="password" class="form-control" required autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary" :disabled="connectingAgent">
<template v-if="connectingAgent">{{ $t("connecting") }}</template>
<template v-else>{{ $t("connect") }}</template>
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
<h2 class="mb-3">{{ $t("Docker Run") }}</h2>
<div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</div>
<button class="btn-normal btn" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
</div> </div>
</transition> </transition>
<router-view ref="child" /> <router-view ref="child" />
</template> </template>
<script> <script>
import { statusNameShort } from "../../../backend/util-common"; import { statusNameShort } from "../../../common/util-common";
export default { export default {
components: { components: {
@ -58,6 +119,14 @@ export default {
importantHeartBeatListLength: 0, importantHeartBeatListLength: 0,
displayedRecords: [], displayedRecords: [],
dockerRunCommand: "", dockerRunCommand: "",
showAgentForm: false,
showRemoveAgentDialog: {},
connectingAgent: false,
agent: {
url: "http://",
username: "",
password: "",
}
}; };
}, },
@ -98,11 +167,43 @@ export default {
methods: { methods: {
addAgent() {
this.connectingAgent = true;
this.$root.getSocket().emit("addAgent", this.agent, (res) => {
this.$root.toastRes(res);
if (res.ok) {
this.showAgentForm = false;
this.agent = {
url: "http://",
username: "",
password: "",
};
}
this.connectingAgent = false;
});
},
removeAgent(url) {
this.$root.getSocket().emit("removeAgent", url, (res) => {
if (res.ok) {
this.$root.toastRes(res);
let urlObj = new URL(url);
let endpoint = urlObj.host;
// Remove the stack list and status list of the removed agent
delete this.$root.allAgentStackList[endpoint];
}
});
},
getStatusNum(statusName) { getStatusNum(statusName) {
let num = 0; let num = 0;
for (let stackName in this.$root.stackList) { for (let stackName in this.$root.completeStackList) {
const stack = this.$root.stackList[stackName]; const stack = this.$root.completeStackList[stackName];
if (statusNameShort(stack.status) === statusName) { if (statusNameShort(stack.status) === statusName) {
num += 1; num += 1;
} }
@ -230,4 +331,20 @@ table {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 15px; font-size: 15px;
} }
.first-row .shadow-box {
}
.remove-agent {
cursor: pointer;
color: rgba(255, 255, 255, 0.3);
}
.agent {
a {
text-decoration: none;
}
}
</style> </style>

View File

@ -35,22 +35,33 @@ const routes = [
component: Compose, component: Compose,
}, },
{ {
path: "/compose/:stackName", path: "/compose/:stackName/:endpoint",
name: "compose", component: Compose,
},
{
path: "/compose/:stackName",
component: Compose, component: Compose,
props: true,
}, },
{ {
path: "/terminal/:stackName/:serviceName/:type", path: "/terminal/:stackName/:serviceName/:type",
component: ContainerTerminal, component: ContainerTerminal,
name: "containerTerminal", name: "containerTerminal",
}, },
{
path: "/terminal/:stackName/:serviceName/:type/:endpoint",
component: ContainerTerminal,
name: "containerTerminalEndpoint",
},
] ]
}, },
{ {
path: "/console", path: "/console",
component: Console, component: Console,
}, },
{
path: "/console/:endpoint",
component: Console,
},
{ {
path: "/settings", path: "/settings",
component: Settings, component: Settings,

View File

@ -1,6 +1,6 @@
{ {
"name": "dockge", "name": "dockge",
"version": "1.3.3", "version": "1.4.0",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">= 18.0.0 && <= 18.17.1" "node": ">= 18.0.0 && <= 18.17.1"
@ -14,9 +14,11 @@
"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",
"release-beta": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker-beta",
"build:frontend": "vite build --config ./frontend/vite.config.ts", "build:frontend": "vite build --config ./frontend/vite.config.ts",
"build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push", "build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
"build:docker": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:$VERSION --target release -f ./docker/Dockerfile . --push", "build:docker": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:$VERSION -t louislam/dockge:beta -t louislam/dockge:nightly --target release -f ./docker/Dockerfile . --push",
"build:docker-beta": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:beta -t louislam/dockge:$VERSION --target release -f ./docker/Dockerfile . --push",
"build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push", "build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push",
"build:healthcheck": "docker buildx build -f docker/BuildHealthCheck.Dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:build-healthcheck . --push", "build:healthcheck": "docker buildx build -f docker/BuildHealthCheck.Dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:build-healthcheck . --push",
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest", "start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest",
@ -46,6 +48,7 @@
"mysql2": "~3.6.5", "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",
"semver": "^7.5.4",
"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",
@ -55,8 +58,6 @@
"yaml": "~2.3.4" "yaml": "~2.3.4"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.2",
"wait-on": "^7.2.0",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@fontsource/jetbrains-mono": "^5.0.18", "@fontsource/jetbrains-mono": "^5.0.18",
"@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/fontawesome-svg-core": "6.4.2",
@ -68,11 +69,15 @@
"@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",
"@types/semver": "^7.5.6",
"@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.2", "@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",
@ -81,16 +86,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.7", "vite": "~5.0.10",
"vite-plugin-compression": "~0.5.1", "vite-plugin-compression": "~0.5.1",
"vue": "~3.3.11", "vue": "~3.3.13",
"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"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
"backend/**/*" "backend/**/*",
"common/**/*"
] ]
} }