forked from extern/dockge
Compare commits
119 Commits
Author | SHA1 | Date | |
---|---|---|---|
532bb6e2ee | |||
789a034fa3 | |||
cacee2221e | |||
fa0a4f8ccf | |||
ca3bb30ee0 | |||
03a1105e34 | |||
17f9ee63f7 | |||
0c32171acc | |||
7d91c8d037 | |||
37f261480a | |||
5056df2644 | |||
c581bcd9db | |||
d655a8cc21 | |||
d99f21fe93 | |||
0f79b46769 | |||
80e885e85d | |||
e54ede3f1c | |||
ac2a62abb1 | |||
e77ff3622d | |||
b5bd9a711a | |||
442c7fce67 | |||
7d55a84aa2 | |||
22bbba9652 | |||
3bc6779af4 | |||
3ef2be1c11 | |||
f6f7283f09 | |||
69e237a676 | |||
6a3eebfd57 | |||
3c137122b6 | |||
e2819afce1 | |||
94ca8a152a | |||
db0add7e4c | |||
0f52bb78b8 | |||
03c7815b58 | |||
80d5c685e5 | |||
a8482ec8ac | |||
07c52ccebb | |||
587d2dcaca | |||
5f1f3593fd | |||
316c566c76 | |||
d32bd3937f | |||
007eac7b58 | |||
b945ddea55 | |||
9b6b49947c | |||
3ba267a3dc | |||
01411f2d7e | |||
ba51031db6 | |||
daa8d12eee | |||
bd58de535e | |||
8c6bcef987 | |||
bec5460395 | |||
4e899dcf21 | |||
8296c7b18f | |||
607c908f2d | |||
bd5dd3c3ad | |||
6eca6dc59f | |||
54fb2c1ef4 | |||
562abb485d | |||
86bed768ea | |||
b79db2375f | |||
b586cca711 | |||
793a9de50d | |||
0df3fee3f4 | |||
05b79ba50e | |||
a3c4082800 | |||
027d9e9b59 | |||
80876a463d | |||
beefe41264 | |||
e4f9b9c9fe | |||
a85813ab95 | |||
47debeddc9 | |||
9ce68f67fa | |||
c3b9db8549 | |||
39279cc7df | |||
f4eeb38d18 | |||
49187577ca | |||
7ffb36ec54 | |||
cd596b2d37 | |||
1711e4f47c | |||
2c0fb7f7e0 | |||
ef7f26b142 | |||
80b907577f | |||
e54211de1b | |||
fc42639cb5 | |||
fde2ef4869 | |||
d554adb0c7 | |||
c1e788d22a | |||
972179b4a8 | |||
d5e1ce51ed | |||
9155cf14a5 | |||
17eb6583df | |||
b717fc6655 | |||
da50860211 | |||
340ea0abe9 | |||
d53cb7ca71 | |||
e29eed4602 | |||
8e9f2209c2 | |||
a8d95d06b9 | |||
e2c81bd3e0 | |||
bf2d4c95f9 | |||
b4aa1b83a2 | |||
157a74aafc | |||
b7a2bab808 | |||
71446b9eb9 | |||
06dbc3fa28 | |||
a0fca4df4d | |||
694923cd42 | |||
bfaa8fd795 | |||
d5721dd8ca | |||
5c35b09e93 | |||
9a5d403219 | |||
9ca65ec94d | |||
5b02b63c95 | |||
88d33aace8 | |||
b5f6919bab | |||
0546f6a24e | |||
7385d216a3 | |||
631bc60cb2 | |||
d23e2d8aa1 |
3
.github/DISCUSSION_TEMPLATE/ask-for-help.yml
vendored
3
.github/DISCUSSION_TEMPLATE/ask-for-help.yml
vendored
@ -1,5 +1,4 @@
|
|||||||
title: "❓ Ask for help"
|
labels: [help]
|
||||||
labels: [help]
|
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: no-duplicate-issues
|
id: no-duplicate-issues
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
title: 🚀 Feature Request
|
|
||||||
labels: [feature-request]
|
labels: [feature-request]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
@ -52,4 +51,4 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "📝 Additional Context"
|
label: "📝 Additional Context"
|
||||||
description: "Add any other context or screenshots about the feature request here."
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
placeholder: "..."
|
placeholder: "..."
|
||||||
|
8
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
8
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@ -1,14 +1,14 @@
|
|||||||
name: "❓ Ask for help"
|
name: "⚠️ Ask for help (Please go to the \"Discussions\" tab to submit a Help Request)"
|
||||||
description: "Please go to the Discussions tab to submit a Help Request"
|
description: "⚠️ Please go to the \"Discussions\" tab to submit a Help Request"
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
|
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: no-duplicate-issues
|
id: no-duplicate-issues
|
||||||
attributes:
|
attributes:
|
||||||
label: "Issues are for bug reports only"
|
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
|
||||||
options:
|
options:
|
||||||
- label: "I understand"
|
- label: "I understand"
|
||||||
required: true
|
required: true
|
||||||
|
8
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
8
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -1,14 +1,14 @@
|
|||||||
name: 🚀 Feature Request
|
name: 🚀 Feature Request (Please go to the "Discussions" tab to submit a Feature Request)
|
||||||
description: "Please go to the Discussions tab to submit a Feature Request"
|
description: "⚠️ Please go to the \"Discussions\" tab to submit a Feature Request"
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Please go to https://github.com/louislam/dockge/discussions/new?category=feature-request
|
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: no-duplicate-issues
|
id: no-duplicate-issues
|
||||||
attributes:
|
attributes:
|
||||||
label: "Issues are for bug reports only"
|
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
|
||||||
options:
|
options:
|
||||||
- label: "I understand"
|
- label: "I understand"
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -14,8 +14,8 @@ jobs:
|
|||||||
ci:
|
ci:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest, ARM64]
|
||||||
node: [20.x] # Can be changed
|
node: [18.17.1] # Can be changed
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
16
.github/workflows/prevent-file-change.yml
vendored
Normal file
16
.github/workflows/prevent-file-change.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name: Prevent File Change
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-file-changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Prevent file change
|
||||||
|
uses: xalvarez/prevent-file-change-action@v1
|
||||||
|
with:
|
||||||
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json
|
||||||
|
pattern: '^(?!frontend/src/lang/en\.json$)frontend/src/lang/.*\.json$'
|
||||||
|
trustedAuthors: UptimeKumaBot
|
@ -7,13 +7,15 @@ Here are some references:
|
|||||||
### ✅ Usually accepted:
|
### ✅ Usually accepted:
|
||||||
- Bug fix
|
- Bug fix
|
||||||
- Security fix
|
- Security fix
|
||||||
- Translation
|
- Adding new language files (see [these instructions](https://github.com/louislam/dockge/blob/master/frontend/src/lang/README.md))
|
||||||
|
- Adding new language keys: `$t("...")`
|
||||||
|
|
||||||
### ⚠️ Discussion required:
|
### ⚠️ Discussion required:
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New features
|
- New features
|
||||||
|
|
||||||
### ❌ Won't be merged:
|
### ❌ Won't be merged:
|
||||||
|
- A dedicated PR for translating existing languages (see [these instructions](https://github.com/louislam/dockge/blob/master/frontend/src/lang/README.md))
|
||||||
- Do not pass the auto-test
|
- Do not pass the auto-test
|
||||||
- Any breaking changes
|
- Any breaking changes
|
||||||
- Duplicated pull requests
|
- Duplicated pull requests
|
||||||
|
107
README.md
107
README.md
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
|
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
|
||||||
|
|
||||||
      
|
   
|
||||||
|
|
||||||
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
|
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
|
||||||
|
|
||||||
@ -14,32 +14,35 @@ 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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🔧 How to Install
|
## 🔧 How to Install
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman
|
- [Docker](https://docs.docker.com/engine/install/) 20+ / Podman
|
||||||
- (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/)
|
|
||||||
- (Podman only) podman-docker (Debian: `apt install podman-docker`)
|
- (Podman only) podman-docker (Debian: `apt install podman-docker`)
|
||||||
- OS:
|
- OS:
|
||||||
- As long as you can run Docker CE / Podman, it should be fine, but:
|
- Major Linux distros that can run Docker/Podman such as:
|
||||||
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher
|
- ✅ Ubuntu
|
||||||
|
- ✅ Debian (Bullseye or newer)
|
||||||
|
- ✅ Raspbian (Bullseye or newer)
|
||||||
|
- ✅ CentOS
|
||||||
|
- ✅ Fedora
|
||||||
|
- ✅ ArchLinux
|
||||||
|
- ❌ Debian/Raspbian Buster or lower is not supported
|
||||||
|
- ❌ Windows (Will be supported later)
|
||||||
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
|
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
|
||||||
|
|
||||||
### Basic
|
### Basic
|
||||||
@ -48,14 +51,14 @@ Requirements:
|
|||||||
- Default Port: 5001
|
- Default Port: 5001
|
||||||
|
|
||||||
```
|
```
|
||||||
# Create a directory that stores your stacks and stores dockge's compose.yaml
|
# Create directories that store your stacks and stores Dockge's stack
|
||||||
mkdir -p /opt/stacks /opt/dockge
|
mkdir -p /opt/stacks /opt/dockge
|
||||||
cd /opt/dockge
|
cd /opt/dockge
|
||||||
|
|
||||||
# Download the compose.yaml
|
# Download the compose.yaml
|
||||||
curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml
|
curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml
|
||||||
|
|
||||||
# Start the Server
|
# Start the server
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# If you are using docker-compose V1 or Podman
|
# If you are using docker-compose V1 or Podman
|
||||||
@ -66,40 +69,24 @@ Dockge is now running on http://localhost:5001
|
|||||||
|
|
||||||
### Advanced
|
### Advanced
|
||||||
|
|
||||||
If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes.
|
If you want to store your stacks in another directory, you can generate your compose.yaml file by using the following URL with custom query strings.
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3.8"
|
|
||||||
services:
|
|
||||||
dockge:
|
|
||||||
image: louislam/dockge:1
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
# Host Port : Container Port
|
|
||||||
- 5001:5001
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./data:/app/data
|
|
||||||
|
|
||||||
# If you want to use private registries, you need to share the auth file with Dockge:
|
|
||||||
# - /root/.docker/:/root/.docker
|
|
||||||
|
|
||||||
# Your stacks directory in the host (The paths inside container must be the same as the host)
|
|
||||||
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
|
|
||||||
# ✔️✔️✔️✔️ CORRECT: - /my-stacks:/my-stacks (Both paths match)
|
|
||||||
# ❌❌❌❌ WRONG: - /docker:/my-stacks (Both paths do not match)
|
|
||||||
- /opt/stacks:/opt/stacks
|
|
||||||
environment:
|
|
||||||
# Tell Dockge where is your stacks directory
|
|
||||||
- DOCKGE_STACKS_DIR=/opt/stacks
|
|
||||||
```
|
```
|
||||||
|
# Download your compose.yaml
|
||||||
|
curl "https://dockge.kuma.pet/compose.yaml?port=5001&stacksPath=/opt/stacks" --output compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
- port=`5001`
|
||||||
|
- stacksPath=`/opt/stacks`
|
||||||
|
|
||||||
|
Interactive compose.yaml generator is available on:
|
||||||
|
https://dockge.kuma.pet
|
||||||
|
|
||||||
## How to Update
|
## How to Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/dockge
|
cd /opt/dockge
|
||||||
docker compose pull
|
docker compose pull && docker compose up -d
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
@ -122,7 +109,7 @@ docker compose up -d
|
|||||||
If you love this project, please consider giving it a ⭐.
|
If you love this project, please consider giving it a ⭐.
|
||||||
|
|
||||||
|
|
||||||
## 🗣️
|
## 🗣️ Community and Contribution
|
||||||
|
|
||||||
### Bug Report
|
### Bug Report
|
||||||
https://github.com/louislam/dockge/issues
|
https://github.com/louislam/dockge/issues
|
||||||
@ -130,15 +117,18 @@ https://github.com/louislam/dockge/issues
|
|||||||
### Ask for Help / Discussions
|
### Ask for Help / Discussions
|
||||||
https://github.com/louislam/dockge/discussions
|
https://github.com/louislam/dockge/discussions
|
||||||
|
|
||||||
## Translation
|
### Translation
|
||||||
|
|
||||||
If you want to translate Dockge into your language, please read [Translation Guide](https://github.com/louislam/dockge/blob/master/frontend/src/lang/README.md)
|
If you want to translate Dockge into your language, please read [Translation Guide](https://github.com/louislam/dockge/blob/master/frontend/src/lang/README.md)
|
||||||
|
|
||||||
|
### Create a Pull Request
|
||||||
|
|
||||||
|
Be sure to read the [guide](https://github.com/louislam/dockge/blob/master/CONTRIBUTING.md), as we don't accept all types of pull requests and don't want to waste your time.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
#### "Dockge"?
|
#### "Dockge"?
|
||||||
|
|
||||||
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Dodge`.
|
"Dockge" is a coinage word which is created by myself. I originally hoped it sounds like `Dodge`, but apparently many people called it `Dockage`, it is also acceptable.
|
||||||
|
|
||||||
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`.
|
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`.
|
||||||
|
|
||||||
@ -155,17 +145,18 @@ Yes, you can. However, you need to move your compose file into the stacks direct
|
|||||||
3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
|
3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
|
||||||
4. Now you should see your stack in the list
|
4. Now you should see your stack in the list
|
||||||
|
|
||||||
## More Ideas?
|
#### Is Dockge a Portainer replacement?
|
||||||
|
|
||||||
- Stats
|
Yes or no. Portainer provides a lot of Docker features. While Dockge is currently only focusing on docker-compose with a better user interface and better user experience.
|
||||||
- File manager
|
|
||||||
- App store for yaml templates
|
|
||||||
- Get app icons
|
|
||||||
- Switch Docker context
|
|
||||||
- Support Dockerfile and build
|
|
||||||
- Support Docker swarm
|
|
||||||
|
|
||||||
|
If you want to manage your container with docker-compose only, the answer may be yes.
|
||||||
|
|
||||||
# Others
|
If you still need to manage something like docker networks, single containers, the answer may be no.
|
||||||
|
|
||||||
|
#### Can I install both Dockge and Portainer?
|
||||||
|
|
||||||
|
Yes, you can.
|
||||||
|
|
||||||
|
## Others
|
||||||
|
|
||||||
Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` also known as `docker-compose.yml`.
|
Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` also known as `docker-compose.yml`.
|
||||||
|
291
backend/agent-manager.ts
Normal file
291
backend/agent-manager.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
7
backend/agent-socket-handler.ts
Normal file
7
backend/agent-socket-handler.ts
Normal 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;
|
||||||
|
}
|
@ -1,51 +1,50 @@
|
|||||||
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, isAdd : unknown, callback) => {
|
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const stack = this.saveStack(socket, server, name, composeYAML, 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, 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, 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") {
|
||||||
throw new ValidationError("Name must be a string");
|
throw new ValidationError("Name must be a string");
|
||||||
}
|
}
|
||||||
const stack = Stack.getStack(server, name);
|
const stack = await Stack.getStack(server, name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stack.delete(socket);
|
await stack.delete(socket);
|
||||||
@ -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", (stackName : unknown, callback) => {
|
agentSocket.on("getStack", async (stackName : unknown, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
@ -73,37 +72,37 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
|
|
||||||
if (stack.isManagedByDockge) {
|
if (stack.isManagedByDockge) {
|
||||||
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);
|
||||||
|
|
||||||
@ -111,12 +110,12 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.start(socket);
|
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);
|
||||||
|
|
||||||
@ -135,12 +134,12 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.stop(socket);
|
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);
|
||||||
|
|
||||||
@ -156,12 +155,12 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.restart(socket);
|
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);
|
||||||
|
|
||||||
@ -177,12 +176,12 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.update(socket);
|
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);
|
||||||
|
|
||||||
@ -198,12 +197,12 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.down(socket);
|
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);
|
||||||
|
|
||||||
@ -219,52 +218,33 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string");
|
throw new ValidationError("Stack name must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName, true);
|
const stack = await Stack.getStack(server, stackName, true);
|
||||||
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
|
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 = 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : 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");
|
||||||
@ -272,12 +252,15 @@ export class DockerSocketHandler extends SocketHandler {
|
|||||||
if (typeof(composeYAML) !== "string") {
|
if (typeof(composeYAML) !== "string") {
|
||||||
throw new ValidationError("Compose YAML must be a string");
|
throw new ValidationError("Compose YAML must be a string");
|
||||||
}
|
}
|
||||||
|
if (typeof(composeENV) !== "string") {
|
||||||
|
throw new ValidationError("Compose ENV must be a string");
|
||||||
|
}
|
||||||
if (typeof(isAdd) !== "boolean") {
|
if (typeof(isAdd) !== "boolean") {
|
||||||
throw new ValidationError("isAdd must be a boolean");
|
throw new ValidationError("isAdd must be a boolean");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = new Stack(server, name, composeYAML);
|
const stack = new Stack(server, name, composeYAML, composeENV, false);
|
||||||
stack.save(isAdd);
|
await stack.save(isAdd);
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
@ -101,19 +87,19 @@ export class TerminalSocketHandler extends SocketHandler {
|
|||||||
log.debug("interactiveTerminal", "Service name: " + serviceName);
|
log.debug("interactiveTerminal", "Service name: " + serviceName);
|
||||||
|
|
||||||
// Get stack
|
// Get stack
|
||||||
const stack = 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);
|
||||||
|
|
||||||
@ -151,20 +137,51 @@ export class TerminalSocketHandler extends SocketHandler {
|
|||||||
throw new ValidationError("Stack name must be a string.");
|
throw new ValidationError("Stack name must be a string.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = Stack.getStack(server, stackName);
|
const stack = await Stack.getStack(server, stackName);
|
||||||
await stack.leaveCombinedTerminal(socket);
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,69 +3,55 @@ import compareVersions from "compare-versions";
|
|||||||
import packageJSON from "../package.json";
|
import packageJSON from "../package.json";
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
|
|
||||||
export const obj = {
|
|
||||||
version: packageJSON.version,
|
|
||||||
latestVersion: null,
|
|
||||||
};
|
|
||||||
export default obj;
|
|
||||||
|
|
||||||
// How much time in ms to wait between update checks
|
// How much time in ms to wait between update checks
|
||||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
||||||
const CHECK_URL = "https://dockge.kuma.pet/version";
|
const CHECK_URL = "https://dockge.kuma.pet/version";
|
||||||
|
|
||||||
let interval : NodeJS.Timeout;
|
class CheckVersion {
|
||||||
|
version = packageJSON.version;
|
||||||
|
latestVersion? : string;
|
||||||
|
interval? : NodeJS.Timeout;
|
||||||
|
|
||||||
export function startInterval() {
|
async startInterval() {
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
if (await Settings.get("checkUpdate") === false) {
|
if (await Settings.get("checkUpdate") === false) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("update-checker", "Retrieving latest versions");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(CHECK_URL);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
// For debug
|
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
|
||||||
data.slow = "1000.0.0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkBeta = await Settings.get("checkBeta");
|
log.debug("update-checker", "Retrieving latest versions");
|
||||||
|
|
||||||
if (checkBeta && data.beta) {
|
try {
|
||||||
if (compareVersions.compare(data.beta, data.slow, ">")) {
|
const res = await fetch(CHECK_URL);
|
||||||
obj.latestVersion = data.beta;
|
const data = await res.json();
|
||||||
return;
|
|
||||||
|
// For debug
|
||||||
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
|
data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkBeta = await Settings.get("checkBeta");
|
||||||
|
|
||||||
|
if (checkBeta && data.beta) {
|
||||||
|
if (compareVersions.compare(data.beta, data.slow, ">")) {
|
||||||
|
this.latestVersion = data.beta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.slow) {
|
||||||
|
this.latestVersion = data.slow;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (_) {
|
||||||
|
log.info("update-checker", "Failed to check for new versions");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.slow) {
|
};
|
||||||
obj.latestVersion = data.slow;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (_) {
|
await check();
|
||||||
log.info("update-checker", "Failed to check for new versions");
|
this.interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
check();
|
|
||||||
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the check update feature
|
|
||||||
* @param value Should the check update feature be enabled?
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export async function enableCheckUpdate(value : boolean) {
|
|
||||||
await Settings.set("checkUpdate", value);
|
|
||||||
|
|
||||||
clearInterval(interval);
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
startInterval();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkVersion = new CheckVersion();
|
||||||
|
export default checkVersion;
|
||||||
|
@ -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";
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export class Docker {
|
|
||||||
|
|
||||||
}
|
|
@ -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,19 +18,24 @@ 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 childProcess from "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";
|
||||||
|
|
||||||
export class DockgeServer {
|
export class DockgeServer {
|
||||||
@ -48,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(),
|
||||||
];
|
];
|
||||||
@ -150,9 +165,6 @@ export class DockgeServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create all the necessary directories
|
|
||||||
this.initDataDir();
|
|
||||||
|
|
||||||
// Create express
|
// Create express
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|
||||||
@ -195,23 +207,88 @@ export class DockgeServer {
|
|||||||
// Create Socket.io
|
// Create Socket.io
|
||||||
this.io = new socketIO.Server(this.httpServer, {
|
this.io = new socketIO.Server(this.httpServer, {
|
||||||
cors,
|
cors,
|
||||||
|
allowRequest: (req, callback) => {
|
||||||
|
let isOriginValid = true;
|
||||||
|
const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
|
||||||
|
|
||||||
|
if (!bypass) {
|
||||||
|
let host = req.headers.host;
|
||||||
|
|
||||||
|
// If this is set, it means the request is from the browser
|
||||||
|
let origin = req.headers.origin;
|
||||||
|
|
||||||
|
// If this is from the browser, check if the origin is allowed
|
||||||
|
if (origin) {
|
||||||
|
try {
|
||||||
|
let originURL = new URL(origin);
|
||||||
|
|
||||||
|
if (host !== originURL.host) {
|
||||||
|
isOriginValid = false;
|
||||||
|
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid origin url, probably not from browser
|
||||||
|
isOriginValid = false;
|
||||||
|
log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("auth", "Origin check is bypassed");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, isOriginValid);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.io.on("connection", async (socket: Socket) => {
|
this.io.on("connection", async (socket: Socket) => {
|
||||||
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
|
||||||
// ***************************
|
// ***************************
|
||||||
@ -219,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", () => {
|
||||||
@ -249,12 +332,20 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async serve() {
|
async serve() {
|
||||||
|
// Create all the necessary directories
|
||||||
|
this.initDataDir();
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
try {
|
try {
|
||||||
await Database.init(this);
|
await Database.init(this);
|
||||||
@ -306,6 +397,7 @@ export class DockgeServer {
|
|||||||
this.sendStackList(true);
|
this.sendStackList(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
checkVersion.startInterval();
|
||||||
});
|
});
|
||||||
|
|
||||||
gracefulShutdown(this.httpServer, {
|
gracefulShutdown(this.httpServer, {
|
||||||
@ -483,26 +575,34 @@ export class DockgeServer {
|
|||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStackList(useCache = false) {
|
/**
|
||||||
let roomList = this.io.sockets.adapter.rooms.keys();
|
* Send stack list to all connected sockets
|
||||||
let map : Map<string, object> | undefined;
|
* @param useCache
|
||||||
|
*/
|
||||||
|
async sendStackList(useCache = false) {
|
||||||
|
let socketList = this.io.sockets.sockets.values();
|
||||||
|
|
||||||
|
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 = 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),
|
||||||
});
|
});
|
||||||
@ -510,27 +610,15 @@ export class DockgeServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStackStatusList() {
|
async getDockerNetworkList() : Promise<string[]> {
|
||||||
let statusList = Stack.getStatusList();
|
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
let roomList = this.io.sockets.adapter.rooms.keys();
|
if (!res.stdout) {
|
||||||
|
return [];
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getDockerNetworkList() : string[] {
|
|
||||||
let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]);
|
|
||||||
let list = res.stdout.toString().split("\n");
|
let list = res.stdout.toString().split("\n");
|
||||||
|
|
||||||
// Remove empty string item
|
// Remove empty string item
|
||||||
@ -568,4 +656,35 @@ export class DockgeServer {
|
|||||||
finalFunction() {
|
finalFunction() {
|
||||||
log.info("server", "Graceful shutdown successful!");
|
log.info("server", "Graceful shutdown successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force connected sockets of a user to refresh and disconnect.
|
||||||
|
* Used for resetting password.
|
||||||
|
* @param {string} userID
|
||||||
|
* @param {string?} currentSocketID
|
||||||
|
*/
|
||||||
|
disconnectAllSocketClients(userID: number | undefined, currentSocketID? : string) {
|
||||||
|
for (const rawSocket of this.io.sockets.sockets.values()) {
|
||||||
|
let socket = rawSocket as DockgeSocket;
|
||||||
|
if ((!userID || socket.userID === userID) && socket.id !== currentSocketID) {
|
||||||
|
try {
|
||||||
|
socket.emit("refresh");
|
||||||
|
socket.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSSL() {
|
||||||
|
return this.config.sslKey && this.config.sslCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalWebSocketURL() {
|
||||||
|
const protocol = this.isSSL() ? "wss" : "ws";
|
||||||
|
const host = this.config.hostname || "localhost";
|
||||||
|
return `${protocol}://${host}:${this.config.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
16
backend/migrations/2023-12-20-2117-agent-table.ts
Normal file
16
backend/migrations/2023-12-20-2117-agent-table.ts
Normal 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
31
backend/models/agent.ts
Normal 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;
|
@ -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 {
|
||||||
|
|
||||||
|
47
backend/socket-handlers/agent-proxy-socket-handler.ts
Normal file
47
backend/socket-handlers/agent-proxy-socket-handler.ts
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
@ -211,6 +220,8 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
await user.resetPassword(password.newPassword);
|
await user.resetPassword(password.newPassword);
|
||||||
|
|
||||||
|
server.disconnectAllSocketClients(user.id, socket.id);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Password has been updated successfully.",
|
msg: "Password has been updated successfully.",
|
||||||
@ -260,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({
|
||||||
@ -280,6 +289,37 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
socket.on("disconnectOtherSocketClients", async () => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
log.warn("disconnectOtherSocketClients", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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> {
|
||||||
|
70
backend/socket-handlers/manage-agent-socket-handler.ts
Normal file
70
backend/socket-handlers/manage-agent-socket-handler.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
230
backend/stack.ts
230
backend/stack.ts
@ -1,10 +1,11 @@
|
|||||||
import { DockgeServer } from "./dockge-server";
|
import { DockgeServer } from "./dockge-server";
|
||||||
import fs from "fs";
|
import fs, { promises as fsAsync } from "fs";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
import yaml from "yaml";
|
import yaml from "yaml";
|
||||||
import { DockgeSocket, ValidationError } from "./util-server";
|
import { DockgeSocket, fileExists, ValidationError } from "./util-server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
|
acceptedComposeFileNames,
|
||||||
COMBINED_TERMINAL_COLS,
|
COMBINED_TERMINAL_COLS,
|
||||||
COMBINED_TERMINAL_ROWS,
|
COMBINED_TERMINAL_ROWS,
|
||||||
CREATED_FILE,
|
CREATED_FILE,
|
||||||
@ -14,15 +15,17 @@ 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 childProcess from "child_process";
|
import childProcessAsync from "promisify-child-process";
|
||||||
|
import { Settings } from "./settings";
|
||||||
|
|
||||||
export class Stack {
|
export class Stack {
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
protected _status: number = UNKNOWN;
|
protected _status: number = UNKNOWN;
|
||||||
protected _composeYAML?: string;
|
protected _composeYAML?: string;
|
||||||
|
protected _composeENV?: string;
|
||||||
protected _configFilePath?: string;
|
protected _configFilePath?: string;
|
||||||
protected _composeFileName: string = "compose.yaml";
|
protected _composeFileName: string = "compose.yaml";
|
||||||
protected server: DockgeServer;
|
protected server: DockgeServer;
|
||||||
@ -31,15 +34,15 @@ export class Stack {
|
|||||||
|
|
||||||
protected static managedStackList: Map<string, Stack> = new Map();
|
protected static managedStackList: Map<string, Stack> = new Map();
|
||||||
|
|
||||||
constructor(server : DockgeServer, name : string, composeYAML? : string, skipFSOperations = false) {
|
constructor(server : DockgeServer, name : string, composeYAML? : string, composeENV? : string, skipFSOperations = false) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this._composeYAML = composeYAML;
|
this._composeYAML = composeYAML;
|
||||||
|
this._composeENV = composeENV;
|
||||||
|
|
||||||
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;
|
||||||
@ -48,32 +51,56 @@ 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,
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of the stack from `docker compose ps --format json`
|
* Get the status of the stack from `docker compose ps --format json`
|
||||||
*/
|
*/
|
||||||
ps() : object {
|
async ps() : Promise<object> {
|
||||||
let res = childProcess.execSync("docker compose ps --format json", {
|
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
|
||||||
cwd: this.path
|
cwd: this.path,
|
||||||
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
return JSON.parse(res.toString());
|
if (!res.stdout) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(res.stdout.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
get isManagedByDockge() : boolean {
|
get isManagedByDockge() : boolean {
|
||||||
@ -92,6 +119,15 @@ export class Stack {
|
|||||||
|
|
||||||
// Check YAML format
|
// Check YAML format
|
||||||
yaml.parse(this.composeYAML);
|
yaml.parse(this.composeYAML);
|
||||||
|
|
||||||
|
let lines = this.composeENV.split("\n");
|
||||||
|
|
||||||
|
// Check if the .env is able to pass docker-compose
|
||||||
|
// Prevent "setenv: The parameter is incorrect"
|
||||||
|
// It only happens when there is one line and it doesn't contain "="
|
||||||
|
if (lines.length === 1 && !lines[0].includes("=") && lines[0].length > 0) {
|
||||||
|
throw new ValidationError("Invalid .env format");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get composeYAML() : string {
|
get composeYAML() : string {
|
||||||
@ -105,6 +141,17 @@ export class Stack {
|
|||||||
return this._composeYAML;
|
return this._composeYAML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get composeENV() : string {
|
||||||
|
if (this._composeENV === undefined) {
|
||||||
|
try {
|
||||||
|
this._composeENV = fs.readFileSync(path.join(this.path, ".env"), "utf-8");
|
||||||
|
} catch (e) {
|
||||||
|
this._composeENV = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._composeENV;
|
||||||
|
}
|
||||||
|
|
||||||
get path() : string {
|
get path() : string {
|
||||||
return path.join(this.server.stacksDir, this.name);
|
return path.join(this.server.stacksDir, this.name);
|
||||||
}
|
}
|
||||||
@ -128,31 +175,39 @@ export class Stack {
|
|||||||
* Save the stack to the disk
|
* Save the stack to the disk
|
||||||
* @param isAdd
|
* @param isAdd
|
||||||
*/
|
*/
|
||||||
save(isAdd : boolean) {
|
async save(isAdd : boolean) {
|
||||||
this.validate();
|
this.validate();
|
||||||
|
|
||||||
let dir = this.path;
|
let dir = this.path;
|
||||||
|
|
||||||
// Check if the name is used if isAdd
|
// Check if the name is used if isAdd
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
if (fs.existsSync(dir)) {
|
if (await fileExists(dir)) {
|
||||||
throw new ValidationError("Stack name already exists");
|
throw new ValidationError("Stack name already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the stack folder
|
// Create the stack folder
|
||||||
fs.mkdirSync(dir);
|
await fsAsync.mkdir(dir);
|
||||||
} else {
|
} else {
|
||||||
if (!fs.existsSync(dir)) {
|
if (!await fileExists(dir)) {
|
||||||
throw new ValidationError("Stack not found");
|
throw new ValidationError("Stack not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write or overwrite the compose.yaml
|
// Write or overwrite the compose.yaml
|
||||||
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
|
await fsAsync.writeFile(path.join(dir, this._composeFileName), this.composeYAML);
|
||||||
|
|
||||||
|
const envPath = path.join(dir, ".env");
|
||||||
|
|
||||||
|
// Write or overwrite the .env
|
||||||
|
// If .env is not existing and the composeENV is empty, we don't need to write it
|
||||||
|
if (await fileExists(envPath) || this.composeENV.trim() !== "") {
|
||||||
|
await fsAsync.writeFile(envPath, this.composeENV);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
async deploy(socket : DockgeSocket) : Promise<number> {
|
||||||
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.");
|
||||||
@ -160,15 +215,15 @@ 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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the stack folder
|
// Remove the stack folder
|
||||||
fs.rmSync(this.path, {
|
await fsAsync.rm(this.path, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
@ -176,8 +231,8 @@ export class Stack {
|
|||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus() {
|
async updateStatus() {
|
||||||
let statusList = Stack.getStatusList();
|
let statusList = await Stack.getStatusList();
|
||||||
let status = statusList.get(this.name);
|
let status = statusList.get(this.name);
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
@ -187,7 +242,27 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
|
/**
|
||||||
|
* Checks if a compose file exists in the specified directory.
|
||||||
|
* @async
|
||||||
|
* @static
|
||||||
|
* @param {string} stacksDir - The directory of the stack.
|
||||||
|
* @param {string} filename - The name of the directory to check for the compose file.
|
||||||
|
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether any compose file exists.
|
||||||
|
*/
|
||||||
|
static async composeFileExists(stacksDir : string, filename : string) : Promise<boolean> {
|
||||||
|
let filenamePath = path.join(stacksDir, filename);
|
||||||
|
// Check if any compose file exists
|
||||||
|
for (const filename of acceptedComposeFileNames) {
|
||||||
|
let composeFile = path.join(filenamePath, filename);
|
||||||
|
if (await fileExists(composeFile)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
|
||||||
let stacksDir = server.stacksDir;
|
let stacksDir = server.stacksDir;
|
||||||
let stackList : Map<string, Stack>;
|
let stackList : Map<string, Stack>;
|
||||||
|
|
||||||
@ -198,16 +273,20 @@ export class Stack {
|
|||||||
stackList = new Map<string, Stack>();
|
stackList = new Map<string, Stack>();
|
||||||
|
|
||||||
// Scan the stacks directory, and get the stack list
|
// Scan the stacks directory, and get the stack list
|
||||||
let filenameList = fs.readdirSync(stacksDir);
|
let filenameList = await fsAsync.readdir(stacksDir);
|
||||||
|
|
||||||
for (let filename of filenameList) {
|
for (let filename of filenameList) {
|
||||||
try {
|
try {
|
||||||
// Check if it is a directory
|
// Check if it is a directory
|
||||||
let stat = fs.statSync(path.join(stacksDir, filename));
|
let stat = await fsAsync.stat(path.join(stacksDir, filename));
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let stack = this.getStack(server, filename);
|
// If no compose file exists, skip it
|
||||||
|
if (!await Stack.composeFileExists(stacksDir, filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let stack = await this.getStack(server, filename);
|
||||||
stack._status = CREATED_FILE;
|
stack._status = CREATED_FILE;
|
||||||
stackList.set(filename, stack);
|
stackList.set(filename, stack);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -222,8 +301,15 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get status from docker compose ls
|
// Get status from docker compose ls
|
||||||
let res = childProcess.execSync("docker compose ls --all --format json");
|
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
||||||
let composeList = JSON.parse(res.toString());
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.stdout) {
|
||||||
|
return stackList;
|
||||||
|
}
|
||||||
|
|
||||||
|
let composeList = JSON.parse(res.stdout.toString());
|
||||||
|
|
||||||
for (let composeStack of composeList) {
|
for (let composeStack of composeList) {
|
||||||
let stack = stackList.get(composeStack.Name);
|
let stack = stackList.get(composeStack.Name);
|
||||||
@ -249,11 +335,18 @@ export class Stack {
|
|||||||
* Get the status list, it will be used to update the status of the stacks
|
* Get the status list, it will be used to update the status of the stacks
|
||||||
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
||||||
*/
|
*/
|
||||||
static getStatusList() : Map<string, number> {
|
static async getStatusList() : Promise<Map<string, number>> {
|
||||||
let statusList = new Map<string, number>();
|
let statusList = new Map<string, number>();
|
||||||
|
|
||||||
let res = childProcess.execSync("docker compose ls --all --format json");
|
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
||||||
let composeList = JSON.parse(res.toString());
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.stdout) {
|
||||||
|
return statusList;
|
||||||
|
}
|
||||||
|
|
||||||
|
let composeList = JSON.parse(res.stdout.toString());
|
||||||
|
|
||||||
for (let composeStack of composeList) {
|
for (let composeStack of composeList) {
|
||||||
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
||||||
@ -281,13 +374,13 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack {
|
static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> {
|
||||||
let dir = path.join(server.stacksDir, stackName);
|
let dir = path.join(server.stacksDir, stackName);
|
||||||
|
|
||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) {
|
||||||
// Maybe it is a stack managed by docker compose directly
|
// Maybe it is a stack managed by docker compose directly
|
||||||
let stackList = this.getStackList(server, true);
|
let stackList = await this.getStackList(server, true);
|
||||||
let stack = stackList.get(stackName);
|
let stack = stackList.get(stackName);
|
||||||
|
|
||||||
if (stack) {
|
if (stack) {
|
||||||
@ -306,7 +399,7 @@ export class Stack {
|
|||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
stack = new Stack(server, stackName);
|
stack = new Stack(server, stackName);
|
||||||
} else {
|
} else {
|
||||||
stack = new Stack(server, stackName, undefined, true);
|
stack = new Stack(server, stackName, undefined, undefined, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
stack._status = UNKNOWN;
|
stack._status = UNKNOWN;
|
||||||
@ -315,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.");
|
||||||
@ -324,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.");
|
||||||
@ -333,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.");
|
||||||
@ -342,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.");
|
||||||
@ -351,14 +444,14 @@ 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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the stack is not running, we don't need to restart it
|
// If the stack is not running, we don't need to restart it
|
||||||
this.updateStatus();
|
await this.updateStatus();
|
||||||
log.debug("update", "Status: " + this.status);
|
log.debug("update", "Status: " + this.status);
|
||||||
if (this.status !== RUNNING) {
|
if (this.status !== RUNNING) {
|
||||||
return exitCode;
|
return exitCode;
|
||||||
@ -372,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;
|
||||||
@ -382,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);
|
||||||
@ -390,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) {
|
||||||
@ -406,24 +499,35 @@ export class Stack {
|
|||||||
async getServiceStatusList() {
|
async getServiceStatusList() {
|
||||||
let statusList = new Map<string, number>();
|
let statusList = new Map<string, number>();
|
||||||
|
|
||||||
let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], {
|
try {
|
||||||
cwd: this.path,
|
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
|
||||||
});
|
cwd: this.path,
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
let lines = res.stdout.toString().split("\n");
|
if (!res.stdout) {
|
||||||
|
return statusList;
|
||||||
for (let line of lines) {
|
|
||||||
try {
|
|
||||||
let obj = JSON.parse(line);
|
|
||||||
if (obj.Health === "") {
|
|
||||||
statusList.set(obj.Service, obj.State);
|
|
||||||
} else {
|
|
||||||
statusList.set(obj.Service, obj.Health);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lines = res.stdout?.toString().split("\n");
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
try {
|
||||||
|
let obj = JSON.parse(line);
|
||||||
|
if (obj.Health === "") {
|
||||||
|
statusList.set(obj.Service, obj.State);
|
||||||
|
} else {
|
||||||
|
statusList.set(obj.Service, obj.Health);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusList;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("getServiceStatusList", e);
|
||||||
|
return statusList;
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusList;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,10 @@ import { LimitQueue } from "./utils/limit-queue";
|
|||||||
import { DockgeSocket } from "./util-server";
|
import { DockgeSocket } from "./util-server";
|
||||||
import {
|
import {
|
||||||
allowedCommandList, allowedRawKeys,
|
allowedCommandList, allowedRawKeys,
|
||||||
getComposeTerminalName,
|
|
||||||
getCryptoRandomInt,
|
|
||||||
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";
|
||||||
|
|
||||||
@ -36,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;
|
||||||
@ -69,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) {
|
||||||
@ -83,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...");
|
||||||
@ -113,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -138,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);
|
||||||
@ -158,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() {
|
||||||
@ -207,14 +224,20 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise<number> {
|
public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise<number> {
|
||||||
const terminal = new Terminal(server, terminalName, file, args, cwd);
|
return new Promise((resolve, reject) => {
|
||||||
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
// check if terminal exists
|
||||||
|
if (Terminal.terminalMap.has(terminalName)) {
|
||||||
|
reject("Another operation is already running, please try again later.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (socket) {
|
let terminal = new Terminal(server, terminalName, file, args, cwd);
|
||||||
terminal.join(socket);
|
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
||||||
}
|
|
||||||
|
if (socket) {
|
||||||
|
terminal.join(socket);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
terminal.onExit((exitCode : number) => {
|
terminal.onExit((exitCode : number) => {
|
||||||
resolve(exitCode);
|
resolve(exitCode);
|
||||||
});
|
});
|
||||||
|
@ -2,9 +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 { AgentManager } from "./agent-manager";
|
||||||
|
|
||||||
export interface JWTDecoded {
|
export interface JWTDecoded {
|
||||||
username : string;
|
username : string;
|
||||||
@ -14,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
|
||||||
@ -55,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?");
|
||||||
@ -82,3 +97,9 @@ export async function doubleCheckPassword(socket : DockgeSocket, currentPassword
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fileExists(file : string) {
|
||||||
|
return fs.promises.access(file, fs.constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
15
common/agent-socket.ts
Normal file
15
common/agent-socket.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
* Common utilities for backend and frontend
|
* Common utilities for backend and frontend
|
||||||
*/
|
*/
|
||||||
import { Document } from "yaml";
|
import yaml, { Document, Pair, Scalar } from "yaml";
|
||||||
|
import { DotenvParseOutput } from "dotenv";
|
||||||
|
|
||||||
// Init dayjs
|
// Init dayjs
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
// @ts-ignore
|
||||||
|
import { replaceVariablesSync } from "@inventage/envsubst";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@ -17,6 +21,11 @@ export interface LooseObject {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseRes {
|
||||||
|
ok: boolean;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
let randomBytes : (numBytes: number) => Uint8Array;
|
let randomBytes : (numBytes: number) => Uint8Array;
|
||||||
initRandomBytes();
|
initRandomBytes();
|
||||||
|
|
||||||
@ -34,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;
|
||||||
@ -107,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
|
||||||
@ -190,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-" + container + "-" + index;
|
return "container-exec-" + endpoint + "-" + stackName + "-" + container + "-" + index;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyYAMLComments(doc : Document, src : Document) {
|
export function copyYAMLComments(doc : Document, src : Document) {
|
||||||
@ -273,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;
|
||||||
|
|
||||||
@ -340,3 +357,53 @@ export function parseDockerPort(input : string, defaultHostname : string = "loca
|
|||||||
display: display,
|
display: display,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function envsubst(string : string, variables : LooseObject) : string {
|
||||||
|
return replaceVariablesSync(string, variables)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse all values in the yaml and for each value, if there are template variables, replace it environment variables
|
||||||
|
* Emulates the behavior of how docker-compose handles environment variables in yaml files
|
||||||
|
* @param content Yaml string
|
||||||
|
* @param env Environment variables
|
||||||
|
* @returns string Yaml string with environment variables replaced
|
||||||
|
*/
|
||||||
|
export function envsubstYAML(content : string, env : DotenvParseOutput) : string {
|
||||||
|
const doc = yaml.parseDocument(content);
|
||||||
|
if (doc.contents) {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const item of doc.contents.items) {
|
||||||
|
traverseYAML(item, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for envsubstYAML(...)
|
||||||
|
* @param pair
|
||||||
|
* @param env
|
||||||
|
*/
|
||||||
|
function traverseYAML(pair : Pair, env : DotenvParseOutput) : void {
|
||||||
|
// @ts-ignore
|
||||||
|
if (pair.value && pair.value.items) {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const item of pair.value.items) {
|
||||||
|
if (item instanceof Pair) {
|
||||||
|
traverseYAML(item, env);
|
||||||
|
} else if (item instanceof Scalar) {
|
||||||
|
let value = item.value as unknown;
|
||||||
|
|
||||||
|
if (typeof(value) === "string") {
|
||||||
|
item.value = envsubst(value, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (pair.value && typeof(pair.value.value) === "string") {
|
||||||
|
// @ts-ignore
|
||||||
|
pair.value.value = envsubst(pair.value.value, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,10 @@ services:
|
|||||||
# If you want to use private registries, you need to share the auth file with Dockge:
|
# If you want to use private registries, you need to share the auth file with Dockge:
|
||||||
# - /root/.docker/:/root/.docker
|
# - /root/.docker/:/root/.docker
|
||||||
|
|
||||||
# Your stacks directory in the host (The paths inside container must be the same as the host)
|
# Stacks Directory
|
||||||
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
|
# ⚠️ READ IT CAREFULLY. If you did it wrong, your data could end up writing into a WRONG PATH.
|
||||||
# ✔️✔️✔️✔️ CORRECT: - /my-stacks:/my-stacks (Both paths match)
|
# ⚠️ 1. FULL path only. No relative path (MUST)
|
||||||
# ❌❌❌❌ WRONG: - /docker:/my-stacks (Both paths do not match)
|
# ⚠️ 2. Left Stacks Path === Right Stacks Path (MUST)
|
||||||
- /opt/stacks:/opt/stacks
|
- /opt/stacks:/opt/stacks
|
||||||
environment:
|
environment:
|
||||||
# Tell Dockge where is your stacks directory
|
# Tell Dockge where is your stacks directory
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
FROM node:20-bookworm-slim
|
# Due to the bug of #145, Node.js's version cannot be changed, unless upstream is fixed.
|
||||||
|
FROM node:18.17.1-bookworm-slim
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
# COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
|
||||||
|
|
||||||
RUN apt update && apt install --yes --no-install-recommends \
|
RUN apt update && apt install --yes --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -24,16 +22,3 @@ RUN apt update && apt install --yes --no-install-recommends \
|
|||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& npm install pnpm -g \
|
&& npm install pnpm -g \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# ensures that /var/run/docker.sock exists
|
|
||||||
# changes the ownership of /var/run/docker.sock
|
|
||||||
RUN touch /var/run/docker.sock && chown node:node /var/run/docker.sock
|
|
||||||
|
|
||||||
# Full Base Image
|
|
||||||
# MariaDB, Chromium and fonts
|
|
||||||
#FROM base-slim AS base
|
|
||||||
#ENV DOCKGE_ENABLE_EMBEDDED_MARIADB=1
|
|
||||||
#RUN apt update && \
|
|
||||||
# apt --yes --no-install-recommends install mariadb-server && \
|
|
||||||
# rm -rf /var/lib/apt/lists/* && \
|
|
||||||
# apt --yes autoremove
|
|
||||||
|
10
docker/BuildHealthCheck.Dockerfile
Normal file
10
docker/BuildHealthCheck.Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
############################################
|
||||||
|
# Build in Golang
|
||||||
|
############################################
|
||||||
|
FROM golang:1.21.4-bookworm
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
COPY ./extra/healthcheck.go ./extra/healthcheck.go
|
||||||
|
|
||||||
|
# Compile healthcheck.go
|
||||||
|
RUN go build -x -o ./extra/healthcheck ./extra/healthcheck.go
|
@ -1,3 +1,8 @@
|
|||||||
|
############################################
|
||||||
|
# Healthcheck Binary
|
||||||
|
############################################
|
||||||
|
FROM louislam/dockge:build-healthcheck AS build_healthcheck
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
# Build
|
# Build
|
||||||
############################################
|
############################################
|
||||||
@ -12,16 +17,17 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l
|
|||||||
############################################
|
############################################
|
||||||
FROM louislam/dockge:base AS release
|
FROM louislam/dockge:base AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --chown=node:node . .
|
COPY --chown=node:node --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
|
||||||
COPY --from=build /app/node_modules /app/node_modules
|
COPY --from=build /app/node_modules /app/node_modules
|
||||||
|
COPY --chown=node:node . .
|
||||||
RUN mkdir ./data
|
RUN mkdir ./data
|
||||||
|
|
||||||
VOLUME /app/data
|
VOLUME /app/data
|
||||||
EXPOSE 5001
|
EXPOSE 5001
|
||||||
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=60s --retries=5 CMD extra/healthcheck
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["tsx", "./backend/index.ts"]
|
CMD ["tsx", "./backend/index.ts"]
|
||||||
|
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
# Mark as Nightly
|
# Mark as Nightly
|
||||||
############################################
|
############################################
|
||||||
|
74
extra/healthcheck.go
Normal file
74
extra/healthcheck.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* If changed, have to run `npm run build-docker-builder-go`.
|
||||||
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Is K8S + "dockge" as the container name
|
||||||
|
// See https://github.com/louislam/uptime-kuma/pull/2083
|
||||||
|
isK8s := strings.HasPrefix(os.Getenv("DOCKGE_PORT"), "tcp://")
|
||||||
|
|
||||||
|
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 28 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
sslKey := os.Getenv("DOCKGE_SSL_KEY")
|
||||||
|
sslCert := os.Getenv("DOCKGE_SSL_CERT")
|
||||||
|
|
||||||
|
hostname := os.Getenv("DOCKGE_HOST")
|
||||||
|
if len(hostname) == 0 {
|
||||||
|
hostname = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := ""
|
||||||
|
// DOCKGE_PORT is override by K8S unexpectedly,
|
||||||
|
if !isK8s {
|
||||||
|
port = os.Getenv("DOCKGE_PORT")
|
||||||
|
}
|
||||||
|
if len(port) == 0 {
|
||||||
|
port = "5001"
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := ""
|
||||||
|
if len(sslKey) != 0 && len(sslCert) != 0 {
|
||||||
|
protocol = "https"
|
||||||
|
} else {
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
url := protocol + "://" + hostname + ":" + port
|
||||||
|
|
||||||
|
log.Println("Checking " + url)
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
|
||||||
|
|
||||||
|
}
|
@ -1,19 +1,37 @@
|
|||||||
// 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 = `
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
|
||||||
### 🆕 New Features
|
### 🆕 New Features
|
||||||
|
-
|
||||||
|
|
||||||
### Improvements
|
### ⬆️ Improvements
|
||||||
|
-
|
||||||
|
|
||||||
### 🐞 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
-
|
||||||
|
|
||||||
### 🦎 Translation Contributions
|
### 🦎 Translation Contributions
|
||||||
|
-
|
||||||
|
|
||||||
|
### ⬆️ Security Fixes
|
||||||
|
-
|
||||||
|
|
||||||
### Others
|
### Others
|
||||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
|
-
|
||||||
|
|
||||||
|
Please let me know if your username is missing, if your pull request has been merged in this version, or your commit has been included in one of the pull requests.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||||
@ -37,6 +55,12 @@ for (const line of lines) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = message.split("* ").pop();
|
message = message.split("* ").pop();
|
||||||
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
|
||||||
|
let thanks = "";
|
||||||
|
if (username != "@louislam") {
|
||||||
|
thanks = `(Thanks ${username})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(pullRequestID, message, thanks);
|
||||||
}
|
}
|
||||||
console.log(template);
|
console.log(template);
|
||||||
|
129
extra/reset-password.ts
Normal file
129
extra/reset-password.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { Database } from "../backend/database";
|
||||||
|
import { R } from "redbean-node";
|
||||||
|
import readline from "readline";
|
||||||
|
import { User } from "../backend/models/user";
|
||||||
|
import { DockgeServer } from "../backend/dockge-server";
|
||||||
|
import { log } from "../backend/log";
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { BaseRes } from "../common/util-common";
|
||||||
|
|
||||||
|
console.log("== Dockge Reset Password Tool ==");
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = new DockgeServer();
|
||||||
|
|
||||||
|
export const main = async () => {
|
||||||
|
// Check if
|
||||||
|
console.log("Connecting the database");
|
||||||
|
try {
|
||||||
|
await Database.init(server);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
log.error("server", "Failed to connect to your database: " + e.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
const user = await R.findOne("user");
|
||||||
|
if (! user) {
|
||||||
|
throw new Error("user not found, have you installed?");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Found user: " + user.username);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let password = await question("New Password: ");
|
||||||
|
let confirmPassword = await question("Confirm New Password: ");
|
||||||
|
|
||||||
|
if (password === confirmPassword) {
|
||||||
|
await User.resetPassword(user.id, password);
|
||||||
|
|
||||||
|
// Reset all sessions by reset jwt secret
|
||||||
|
await server.initJWTSecret();
|
||||||
|
|
||||||
|
console.log("Password reset successfully.");
|
||||||
|
|
||||||
|
// Disconnect all other socket clients of the user
|
||||||
|
await disconnectAllSocketClients(user.username, password);
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
console.log("Passwords do not match, please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error("Error: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log("Finished.");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask question of user
|
||||||
|
* @param question Question to ask
|
||||||
|
* @returns Users response
|
||||||
|
*/
|
||||||
|
function question(question : string) : Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectAllSocketClients(username : string, password : string) : Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const url = server.getLocalWebSocketURL();
|
||||||
|
|
||||||
|
console.log("Connecting to " + url + " to disconnect all other socket clients");
|
||||||
|
|
||||||
|
// Disconnect all socket connections
|
||||||
|
const socket = io(url, {
|
||||||
|
reconnection: false,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit("login", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}, (res : BaseRes) => {
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("Logged in.");
|
||||||
|
socket.emit("disconnectOtherSocketClients");
|
||||||
|
} else {
|
||||||
|
console.warn("Login failed.");
|
||||||
|
console.warn("Please restart the server to disconnect all sessions.");
|
||||||
|
}
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", function () {
|
||||||
|
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
|
||||||
|
// Ask the user to restart the server manually
|
||||||
|
console.warn("Failed to connect to " + url);
|
||||||
|
console.warn("Please restart the server to disconnect all sessions manually.");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_BACKEND) {
|
||||||
|
main();
|
||||||
|
}
|
@ -30,6 +30,10 @@ export default {
|
|||||||
displayName: {
|
displayName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
},
|
||||||
|
objectType: {
|
||||||
|
type: String,
|
||||||
|
default: "service",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -41,8 +45,7 @@ export default {
|
|||||||
array() {
|
array() {
|
||||||
// Create the array if not exists, it should be safe.
|
// Create the array if not exists, it should be safe.
|
||||||
if (!this.service[this.name]) {
|
if (!this.service[this.name]) {
|
||||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
return [];
|
||||||
this.service[this.name] = [];
|
|
||||||
}
|
}
|
||||||
return this.service[this.name];
|
return this.service[this.name];
|
||||||
},
|
},
|
||||||
@ -56,8 +59,24 @@ export default {
|
|||||||
return this.service[this.name] !== undefined;
|
return this.service[this.name] !== undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not a good name, but it is used to get the object.
|
||||||
|
*/
|
||||||
service() {
|
service() {
|
||||||
return this.$parent.$parent.service;
|
if (this.objectType === "service") {
|
||||||
|
// Used in Container.vue
|
||||||
|
return this.$parent.$parent.service;
|
||||||
|
} else if (this.objectType === "x-dockge") {
|
||||||
|
|
||||||
|
if (!this.$parent.$parent.jsonConfig["x-dockge"]) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used in Compose.vue
|
||||||
|
return this.$parent.$parent.jsonConfig["x-dockge"];
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
valid() {
|
valid() {
|
||||||
@ -81,6 +100,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addField() {
|
addField() {
|
||||||
|
|
||||||
|
// Create the object if not exists.
|
||||||
|
if (this.objectType === "x-dockge") {
|
||||||
|
if (!this.$parent.$parent.jsonConfig["x-dockge"]) {
|
||||||
|
this.$parent.$parent.jsonConfig["x-dockge"] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the array if not exists.
|
||||||
|
if (!this.service[this.name]) {
|
||||||
|
this.service[this.name] = [];
|
||||||
|
}
|
||||||
|
|
||||||
this.array.push("");
|
this.array.push("");
|
||||||
},
|
},
|
||||||
remove(index) {
|
remove(index) {
|
||||||
|
@ -49,8 +49,7 @@ export default {
|
|||||||
array() {
|
array() {
|
||||||
// Create the array if not exists, it should be safe.
|
// Create the array if not exists, it should be safe.
|
||||||
if (!this.service[this.name]) {
|
if (!this.service[this.name]) {
|
||||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
return [];
|
||||||
this.service[this.name] = [];
|
|
||||||
}
|
}
|
||||||
return this.service[this.name];
|
return this.service[this.name];
|
||||||
},
|
},
|
||||||
@ -89,6 +88,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addField() {
|
addField() {
|
||||||
|
// Create the array if not exists.
|
||||||
|
if (!this.service[this.name]) {
|
||||||
|
this.service[this.name] = [];
|
||||||
|
}
|
||||||
this.array.push("");
|
this.array.push("");
|
||||||
},
|
},
|
||||||
remove(index) {
|
remove(index) {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div v-if="!isEditMode">
|
<div v-if="!isEditMode">
|
||||||
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
|
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
|
||||||
|
|
||||||
<a v-for="port in service.ports" :key="port" :href="parsePort(port).url" target="_blank">
|
<a v-for="port in envsubstService.ports" :key="port" :href="parsePort(port).url" target="_blank">
|
||||||
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
|
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -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() {
|
||||||
@ -213,16 +233,29 @@ export default defineComponent({
|
|||||||
jsonObject() {
|
jsonObject() {
|
||||||
return this.$parent.$parent.jsonConfig;
|
return this.$parent.$parent.jsonConfig;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
envsubstJSONConfig() {
|
||||||
|
return this.$parent.$parent.envsubstJSONConfig;
|
||||||
|
},
|
||||||
|
|
||||||
|
envsubstService() {
|
||||||
|
if (!this.envsubstJSONConfig.services[this.name]) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return this.envsubstJSONConfig.services[this.name];
|
||||||
|
},
|
||||||
|
|
||||||
imageName() {
|
imageName() {
|
||||||
if (this.service.image) {
|
if (this.envsubstService.image) {
|
||||||
return this.service.image.split(":")[0];
|
return this.envsubstService.image.split(":")[0];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
imageTag() {
|
imageTag() {
|
||||||
if (this.service.image) {
|
if (this.envsubstService.image) {
|
||||||
let tag = this.service.image.split(":")[1];
|
let tag = this.envsubstService.image.split(":")[1];
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return tag;
|
return tag;
|
||||||
@ -241,8 +274,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
parsePort(port) {
|
parsePort(port) {
|
||||||
let hostname = this.$root.info.primaryHostname || location.hostname;
|
return parseDockerPort(port, this.stack.primaryHostname);
|
||||||
return parseDockerPort(port, hostname);
|
|
||||||
},
|
},
|
||||||
remove() {
|
remove() {
|
||||||
delete this.jsonObject.services[this.name];
|
delete this.jsonObject.services[this.name];
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
@ -152,6 +152,15 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
result.sort((m1, m2) => {
|
result.sort((m1, m2) => {
|
||||||
|
|
||||||
|
// sort by managed by dockge
|
||||||
|
if (m1.isManagedByDockge && !m2.isManagedByDockge) {
|
||||||
|
return -1;
|
||||||
|
} else if (!m1.isManagedByDockge && m2.isManagedByDockge) {
|
||||||
|
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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
@ -19,10 +19,14 @@ const languageList = {
|
|||||||
"ru": "Русский",
|
"ru": "Русский",
|
||||||
"cs-CZ": "Čeština",
|
"cs-CZ": "Čeština",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"th":"ไทย",
|
"th": "ไทย",
|
||||||
"it-IT":"Italiano",
|
"it-IT": "Italiano",
|
||||||
"sv-SE":"Svenska",
|
"sv-SE": "Svenska",
|
||||||
"uk-UA":"Українська"
|
"uk-UA": "Українська",
|
||||||
|
"da": "Dansk",
|
||||||
|
"ja": "日本語",
|
||||||
|
"nl": "Nederlands",
|
||||||
|
"ro": "Română",
|
||||||
};
|
};
|
||||||
|
|
||||||
let messages = {
|
let messages = {
|
||||||
@ -35,7 +39,7 @@ for (let lang in languageList) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rtlLangs = [ "fa", "ar-SY", "ur" ];
|
const rtlLangs = [ "fa", "ar-SY", "ur", "ar" ];
|
||||||
|
|
||||||
export const currentLocale = () => localStorage.locale
|
export const currentLocale = () => localStorage.locale
|
||||||
|| languageList[navigator.language] && navigator.language
|
|| languageList[navigator.language] && navigator.language
|
||||||
|
@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
A simple guide on how to translate `Dockge` in your native language.
|
A simple guide on how to translate `Dockge` in your native language.
|
||||||
|
|
||||||
|
## How to Translate
|
||||||
|
|
||||||
|
(11-26-2023) Updated
|
||||||
|
|
||||||
|
1. Go to <https://weblate.kuma.pet>
|
||||||
|
2. Register an account on Weblate
|
||||||
|
3. Make sure your GitHub email is matched with Weblate's account, so that it could show you as a contributor on GitHub
|
||||||
|
4. Choose your language on Weblate and start translating.
|
||||||
|
|
||||||
## How to add a new language in the dropdown
|
## How to add a new language in the dropdown
|
||||||
|
|
||||||
(11-21-2023) Updated
|
1. Add your Language at <https://weblate.kuma.pet/projects/dockge/dockge/>.
|
||||||
|
2. Find the language code (You can find it at the end of the URL)
|
||||||
1. Add your Language at `frontend/src/lang/` by creating a new file with your language Code, format: `zh-TW.json` .
|
|
||||||
2. Copy the content from `en.json` and make translations from that.
|
|
||||||
3. Add your language at the end of `languageList` in `frontend/src/i18n.ts`, format: `"zh-TW": "繁體中文 (台灣)"`,
|
3. Add your language at the end of `languageList` in `frontend/src/i18n.ts`, format: `"zh-TW": "繁體中文 (台灣)"`,
|
||||||
4. Commit to new branch and make a new Pull Request for me to approve.
|
4. Commit to new branch and make a new Pull Request for me to approve.
|
||||||
|
|
||||||
*Note:* Currently we are only accepting one Pull Request per Language Translate.
|
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
"registry": "السجل",
|
"registry": "السجل",
|
||||||
"compose": "أنشاء كمبوز",
|
"compose": "أنشاء كمبوز",
|
||||||
"addFirstStackMsg": "أنشيء أول كمبوز!",
|
"addFirstStackMsg": "أنشيء أول كمبوز!",
|
||||||
"stackName" : "اسم المكدسة",
|
"stackName": "اسم المكدسة",
|
||||||
"deployStack": "شنر",
|
"deployStack": "نشر",
|
||||||
"deleteStack": "حذف",
|
"deleteStack": "حذف",
|
||||||
"stopStack": "إيقاف",
|
"stopStack": "إيقاف",
|
||||||
"restartStack": "إعادة تشغيل",
|
"restartStack": "إعادة تشغيل",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"editStack": "تعديل",
|
"editStack": "تعديل",
|
||||||
"discardStack": "إهمال",
|
"discardStack": "إهمال",
|
||||||
"saveStackDraft": "حفظ",
|
"saveStackDraft": "حفظ",
|
||||||
"notAvailableShort" : "غير متوفر",
|
"notAvailableShort": "غير متوفر",
|
||||||
"deleteStackMsg": "هل أنت متأكد أنك تريد حذف هذه المكدسة؟",
|
"deleteStackMsg": "هل أنت متأكد أنك تريد حذف هذه المكدسة؟",
|
||||||
"stackNotManagedByDockgeMsg": "لا يتم إدارة هذه المكدس بواسطة Dockge.",
|
"stackNotManagedByDockgeMsg": "لا يتم إدارة هذه المكدس بواسطة Dockge.",
|
||||||
"primaryHostname": "اسم المضيف الرئيسي",
|
"primaryHostname": "اسم المضيف الرئيسي",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"deleteContainer": "حذف",
|
"deleteContainer": "حذف",
|
||||||
"addContainer": "أضافة حاوية",
|
"addContainer": "أضافة حاوية",
|
||||||
"addNetwork": "أضافة شبكة",
|
"addNetwork": "أضافة شبكة",
|
||||||
"disableauth.message1": "هل أنت متأكد أنك تريد <strong>تعطيل المصادقة</strong>?",
|
"disableauth.message1": "هل أنت متأكد أنك تريد <strong>تعطيل المصادقة</strong>؟",
|
||||||
"disableauth.message2": "إنه مصمم للحالات <strong>التي تنوي فيها مصادقة الطرف الثالث</strong> أمام Dockge مثل Cloudflare Access, Authelia أو أي من آليات المصادقة الأخرى.",
|
"disableauth.message2": "إنه مصمم للحالات <strong>التي تنوي فيها مصادقة الطرف الثالث</strong> أمام Dockge مثل Cloudflare Access, Authelia أو أي من آليات المصادقة الأخرى.",
|
||||||
"passwordNotMatchMsg": "كلمة المرور المكررة غير متطابقة.",
|
"passwordNotMatchMsg": "كلمة المرور المكررة غير متطابقة.",
|
||||||
"autoGet": "الجلب التلقائي",
|
"autoGet": "الجلب التلقائي",
|
||||||
@ -91,5 +91,12 @@
|
|||||||
"Allowed commands:": "الأوامر المسموح بها:",
|
"Allowed commands:": "الأوامر المسموح بها:",
|
||||||
"Internal Networks": "الشبكات الداخلية",
|
"Internal Networks": "الشبكات الداخلية",
|
||||||
"External Networks": "الشبكات الخارجية",
|
"External Networks": "الشبكات الخارجية",
|
||||||
"No External Networks": "لا توجد شبكات خارجية"
|
"No External Networks": "لا توجد شبكات خارجية",
|
||||||
|
"reverseProxyMsg2": "تحقق كيف يتم إعداده لمقبس ويب",
|
||||||
|
"Cannot connect to the socket server.": "تعذر الاتصال بخادم المقبس.",
|
||||||
|
"reconnecting...": "إعادة الاتصال…",
|
||||||
|
"url": "رابط | روابط",
|
||||||
|
"extra": "إضافات",
|
||||||
|
"reverseProxyMsg1": "هل تستدخم خادم عكسي؟",
|
||||||
|
"connecting...": "جاري الاتصال بخادم المقبس…"
|
||||||
}
|
}
|
||||||
|
@ -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,13 @@
|
|||||||
"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...": "Свързване със сокет сървъра…"
|
||||||
}
|
}
|
||||||
|
@ -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 ."
|
||||||
}
|
}
|
||||||
|
102
frontend/src/lang/da.json
Normal file
102
frontend/src/lang/da.json
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"languageName": "Dansk",
|
||||||
|
"authIncorrectCreds": "Forkert brugernavn eller adgangskode.",
|
||||||
|
"PasswordsDoNotMatch": "Adgangskode stemmer ikke overens.",
|
||||||
|
"Repeat Password": "Gentag adgangskode",
|
||||||
|
"Create": "Opret",
|
||||||
|
"signedInDisp": "Logget ind som {0}",
|
||||||
|
"signedInDispDisabled": "Auth Deaktiveret.",
|
||||||
|
"home": "Hjem",
|
||||||
|
"console": "Konsol",
|
||||||
|
"registry": "Registry",
|
||||||
|
"compose": "Compose",
|
||||||
|
"stackName": "Stack-navn",
|
||||||
|
"deployStack": "Udrulle",
|
||||||
|
"deleteStack": "Slet",
|
||||||
|
"stopStack": "Stop",
|
||||||
|
"restartStack": "Genstart",
|
||||||
|
"updateStack": "Opdatere",
|
||||||
|
"startStack": "Start",
|
||||||
|
"downStack": "Stop & Sluk",
|
||||||
|
"editStack": "Editere",
|
||||||
|
"discardStack": "Annuller",
|
||||||
|
"saveStackDraft": "Gem",
|
||||||
|
"notAvailableShort": "Ugyldig",
|
||||||
|
"stackNotManagedByDockgeMsg": "Denne stack administreres ikke af Dockge.",
|
||||||
|
"primaryHostname": "Primært værtsnavn",
|
||||||
|
"general": "Generelt",
|
||||||
|
"container": "Container | Containere",
|
||||||
|
"scanFolder": "Scan Stack-mappe",
|
||||||
|
"dockerImage": "Billede",
|
||||||
|
"restartPolicyUnlessStopped": "Medmindre stoppet",
|
||||||
|
"restartPolicyAlways": "Altid",
|
||||||
|
"restartPolicyOnFailure": "Ved fejl",
|
||||||
|
"restartPolicyNo": "Nej",
|
||||||
|
"restartPolicy": "Genstart politik",
|
||||||
|
"containerName": "Container navn",
|
||||||
|
"port": "Port | Porte",
|
||||||
|
"volume": "Volumen | Voluminer",
|
||||||
|
"network": "Netværk | Netværker",
|
||||||
|
"dependsOn": "Container Dependency | Container Dependencies",
|
||||||
|
"addListItem": "Tilføj {0}",
|
||||||
|
"deleteContainer": "Slet",
|
||||||
|
"addNetwork": "Tilføj Netværk",
|
||||||
|
"passwordNotMatchMsg": "Koden du gentog stemmer ikke overens.",
|
||||||
|
"autoGet": "Auto Get",
|
||||||
|
"add": "Tilføj",
|
||||||
|
"Edit": "Redigere",
|
||||||
|
"applyToYAML": "Anvend til YAML",
|
||||||
|
"createExternalNetwork": "Skabe",
|
||||||
|
"addInternalNetwork": "Tilføj",
|
||||||
|
"Save": "Gem",
|
||||||
|
"Language": "Sprog",
|
||||||
|
"Current User": "Nuværende bruger",
|
||||||
|
"Change Password": "Ændre adgangskode",
|
||||||
|
"Current Password": "Nuværende adgangskode",
|
||||||
|
"New Password": "Ny adgangskode",
|
||||||
|
"Repeat New Password": "Gentag ny adgangskode",
|
||||||
|
"Update Password": "Opdater adgangskode",
|
||||||
|
"Advanced": "Avanceret",
|
||||||
|
"Please use this option carefully!": "Brug venligst denne indstilling forsigtigt!",
|
||||||
|
"Enable Auth": "Aktiver Auth",
|
||||||
|
"Disable Auth": "Deaktiver Auth",
|
||||||
|
"I understand, please disable": "Jeg forstår, venligst deaktiver",
|
||||||
|
"Leave": "Forlad",
|
||||||
|
"Frontend Version": "Frontend Version",
|
||||||
|
"Check Update On GitHub": "Tjek opdatering på GitHub",
|
||||||
|
"Also check beta release": "Tjek også betaversionen",
|
||||||
|
"Remember me": "Husk mig",
|
||||||
|
"Login": "Login",
|
||||||
|
"Username": "Brugernavn",
|
||||||
|
"Password": "Adgangskode",
|
||||||
|
"Settings": "Indstillinger",
|
||||||
|
"Logout": "Log ud",
|
||||||
|
"Convert to Compose": "Konverter til Compose",
|
||||||
|
"active": "aktive",
|
||||||
|
"exited": "forladt",
|
||||||
|
"inactive": "inaktive",
|
||||||
|
"Appearance": "Udseende",
|
||||||
|
"Security": "Sikkerhed",
|
||||||
|
"Docker Run": "Docker Kør",
|
||||||
|
"About": "Om",
|
||||||
|
"Allowed commands:": "Tilladte kommandoer:",
|
||||||
|
"Internal Networks": "Interne netværk",
|
||||||
|
"External Networks": "Eksterne netværk",
|
||||||
|
"No External Networks": "Ingen eksterne netværk",
|
||||||
|
"reverseProxyMsg1": "Bruger du en Reverse-Proxy?",
|
||||||
|
"reverseProxyMsg2": "Tjek, hvordan du konfigurerer det til WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Kan ikke oprette forbindelse til socket-serveren.",
|
||||||
|
"reconnecting...": "Genopretter forbindelse…",
|
||||||
|
"connecting...": "Opretter forbindelse til socket-serveren…",
|
||||||
|
"url": "URL | URL'er",
|
||||||
|
"extra": "Ekstra",
|
||||||
|
"Create your admin account": "Opret din administratorkonto",
|
||||||
|
"addFirstStackMsg": "Compose din første stack!",
|
||||||
|
"deleteStackMsg": "Er du sikker på, at du vil slette denne stack?",
|
||||||
|
"environmentVariable": "Miljøvariabel | miljøvariabler",
|
||||||
|
"addContainer": "Tilføj Container",
|
||||||
|
"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.",
|
||||||
|
"Show update if available": "Vis opdatering, hvis tilgængelig",
|
||||||
|
"Lowercase only": "Kun små bogstaver"
|
||||||
|
}
|
@ -6,14 +6,14 @@
|
|||||||
"Repeat Password": "Passwort wiederholen",
|
"Repeat Password": "Passwort wiederholen",
|
||||||
"Create": "Erstellen",
|
"Create": "Erstellen",
|
||||||
"signedInDisp": "Angemeldet als {0}",
|
"signedInDisp": "Angemeldet als {0}",
|
||||||
"signedInDispDisabled": "Authentifizierung deaktiviert.",
|
"signedInDispDisabled": "Anmeldung deaktiviert.",
|
||||||
"home": "Startseite",
|
"home": "Startseite",
|
||||||
"console": "Konsole",
|
"console": "Konsole",
|
||||||
"registry": "Register",
|
"registry": "Container Registry",
|
||||||
"compose": "Zusammenstellen",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
|
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
|
||||||
"stackName" : "Stack-Name",
|
"stackName": "Stack-Name",
|
||||||
"deployStack": "Bereitstellen",
|
"deployStack": "Deployen",
|
||||||
"deleteStack": "Löschen",
|
"deleteStack": "Löschen",
|
||||||
"stopStack": "Anhalten",
|
"stopStack": "Anhalten",
|
||||||
"restartStack": "Neustarten",
|
"restartStack": "Neustarten",
|
||||||
@ -22,33 +22,33 @@
|
|||||||
"editStack": "Bearbeiten",
|
"editStack": "Bearbeiten",
|
||||||
"discardStack": "Verwerfen",
|
"discardStack": "Verwerfen",
|
||||||
"saveStackDraft": "Speichern",
|
"saveStackDraft": "Speichern",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/V",
|
||||||
"deleteStackMsg": "Möchtest du diesen Stack wirklich löschen?",
|
"deleteStackMsg": "Möchtest du diesen Stack wirklich löschen?",
|
||||||
"stackNotManagedByDockgeMsg": "Dieser Stack wird nicht von Dockge verwaltet.",
|
"stackNotManagedByDockgeMsg": "Dieser Stack wird nicht von Dockge verwaltet.",
|
||||||
"primaryHostname": "Primärer Hostname",
|
"primaryHostname": "Primärer Hostname",
|
||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
"container": "Container | Container",
|
"container": "Container",
|
||||||
"scanFolder": "Stacks-Ordner durchsuchen",
|
"scanFolder": "Stacks-Ordner durchsuchen",
|
||||||
"dockerImage": "Image",
|
"dockerImage": "Image",
|
||||||
"restartPolicyUnlessStopped": "Falls nicht gestoppt",
|
"restartPolicyUnlessStopped": "Falls nicht gestoppt",
|
||||||
"restartPolicyAlways": "Immer",
|
"restartPolicyAlways": "Immer",
|
||||||
"restartPolicyOnFailure": "Bei Fehler",
|
"restartPolicyOnFailure": "Bei Fehler",
|
||||||
"restartPolicyNo": "Kein Neustart",
|
"restartPolicyNo": "Kein Neustart",
|
||||||
"environmentVariable": "Umgebungsvariable | Umgebungsvariablen",
|
"environmentVariable": "Umgebungsvariable/n",
|
||||||
"restartPolicy": "Neustart Richtlinie",
|
"restartPolicy": "Neustart Richtlinie",
|
||||||
"containerName": "Container-Name",
|
"containerName": "Container-Name",
|
||||||
"port": "Port | Ports",
|
"port": "Port / Ports",
|
||||||
"volume": "Volume | Volumes",
|
"volume": "Volume / Volumes",
|
||||||
"network": "Netzwerk | Netzwerke",
|
"network": "Netzwerk | Netzwerke",
|
||||||
"dependsOn": "Container-Abhängigkeit | Container-Abhängigkeiten",
|
"dependsOn": "Container-Abhängigkeit/en",
|
||||||
"addListItem": "{0} hinzufügen",
|
"addListItem": "{0} hinzufügen",
|
||||||
"deleteContainer": "Löschen",
|
"deleteContainer": "Löschen",
|
||||||
"addContainer": "Container hinzufügen",
|
"addContainer": "Container hinzufügen",
|
||||||
"addNetwork": "Netzwerk hinzufügen",
|
"addNetwork": "Netzwerk hinzufügen",
|
||||||
"disableauth.message1": "Bist du sicher, dass du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
|
"disableauth.message1": "Bist du sicher, dass du die <strong>Anmeldung deaktivieren</strong> möchtest?",
|
||||||
"disableauth.message2": "Es ist für Szenarien vorgesehen, <strong>in denen du beabsichtigst, eine Drittanbieter-Authentifizierung</strong> vor Dockge zu implementieren, wie zum Beispiel Cloudflare Access, Authelia oder andere Authentifizierungsmechanismen.",
|
"disableauth.message2": "Es ist für Szenarien vorgesehen, <strong>in denen du beabsichtigst, eine Drittanbieter-Authentifizierung</strong> vor Dockge zu implementieren, wie zum Beispiel Cloudflare Access, Authelia oder andere Authentifizierungsmechanismen.",
|
||||||
"passwordNotMatchMsg": "Das wiederholte Passwort stimmt nicht überein.",
|
"passwordNotMatchMsg": "Das wiederholte Passwort stimmt nicht überein.",
|
||||||
"autoGet": "Automatisch holen",
|
"autoGet": "Automatisch laden",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"Edit": "Bearbeiten",
|
"Edit": "Bearbeiten",
|
||||||
"applyToYAML": "Auf YAML anwenden",
|
"applyToYAML": "Auf YAML anwenden",
|
||||||
@ -64,23 +64,23 @@
|
|||||||
"Update Password": "Passwort aktualisieren",
|
"Update Password": "Passwort aktualisieren",
|
||||||
"Advanced": "Erweitert",
|
"Advanced": "Erweitert",
|
||||||
"Please use this option carefully!": "Bitte verwende diese Option sorgfältig!",
|
"Please use this option carefully!": "Bitte verwende diese Option sorgfältig!",
|
||||||
"Enable Auth": "Authentifizierung aktivieren",
|
"Enable Auth": "Anmeldung aktivieren",
|
||||||
"Disable Auth": "Authentifizierung deaktivieren",
|
"Disable Auth": "Anmeldung deaktivieren",
|
||||||
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
|
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
|
||||||
"Leave": "Verlassen",
|
"Leave": "Verlassen",
|
||||||
"Frontend Version": "Frontend Version",
|
"Frontend Version": "Frontend Version",
|
||||||
"Check Update On GitHub": "Update auf GitHub überprüfen",
|
"Check Update On GitHub": "Update auf GitHub überprüfen",
|
||||||
"Show update if available": "Update anzeigen, wenn verfügbar",
|
"Show update if available": "Update anzeigen, wenn verfügbar",
|
||||||
"Also check beta release": "Auch Beta-Version überprüfen",
|
"Also check beta release": "Auch Beta-Version überprüfen",
|
||||||
"Remember me": "Anmeldung beibehalten",
|
"Remember me": "Angemeldet bleiben",
|
||||||
"Login": "Anmelden",
|
"Login": "Anmelden",
|
||||||
"Username": "Benutzername",
|
"Username": "Benutzername",
|
||||||
"Password": "Passwort",
|
"Password": "Passwort",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
"Logout": "Abmelden",
|
"Logout": "Abmelden",
|
||||||
"Lowercase only": "Nur Kleinbuchstaben",
|
"Lowercase only": "Nur Kleinbuchstaben",
|
||||||
"Convert to Compose": "In Compose Syntax umwandeln",
|
"Convert to Compose": "In Compose-Syntax umwandeln",
|
||||||
"Docker Run": "Docker ausführen",
|
"Docker Run": "Docker Run",
|
||||||
"active": "aktiv",
|
"active": "aktiv",
|
||||||
"exited": "beendet",
|
"exited": "beendet",
|
||||||
"inactive": "inaktiv",
|
"inactive": "inaktiv",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Zugelassene Befehle:",
|
"Allowed commands:": "Zugelassene Befehle:",
|
||||||
"Internal Networks": "Interne Netzwerke",
|
"Internal Networks": "Interne Netzwerke",
|
||||||
"External Networks": "Externe Netzwerke",
|
"External Networks": "Externe Netzwerke",
|
||||||
"No External Networks": "Keine externen Netzwerke"
|
"No External Networks": "Keine externen Netzwerke",
|
||||||
}
|
"Cannot connect to the socket server.": "Keine Verbindung zum Socket Server.",
|
||||||
|
"reverseProxyMsg1": "Wird ein Reverse Proxy genutzt?",
|
||||||
|
"reconnecting...": "Erneuter Verbindungsaufbau…",
|
||||||
|
"downStack": "Stoppen & Aus",
|
||||||
|
"extra": "Extra",
|
||||||
|
"url": "URL / URLs",
|
||||||
|
"reverseProxyMsg2": "Lerne wie dieser für WebSockets zu konfigurieren ist.",
|
||||||
|
"connecting...": "Verbindungsaufbau zum Socket Server…"
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"registry": "Registry",
|
"registry": "Registry",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Compose your first stack!",
|
"addFirstStackMsg": "Compose your first stack!",
|
||||||
"stackName" : "Stack Name",
|
"stackName": "Stack Name",
|
||||||
"deployStack": "Deploy",
|
"deployStack": "Deploy",
|
||||||
"deleteStack": "Delete",
|
"deleteStack": "Delete",
|
||||||
"stopStack": "Stop",
|
"stopStack": "Stop",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"editStack": "Edit",
|
"editStack": "Edit",
|
||||||
"discardStack": "Discard",
|
"discardStack": "Discard",
|
||||||
"saveStackDraft": "Save",
|
"saveStackDraft": "Save",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/A",
|
||||||
"deleteStackMsg": "Are you sure you want to delete this stack?",
|
"deleteStackMsg": "Are you sure you want to delete this stack?",
|
||||||
"stackNotManagedByDockgeMsg": "This stack is not managed by Dockge.",
|
"stackNotManagedByDockgeMsg": "This stack is not managed by Dockge.",
|
||||||
"primaryHostname": "Primary Hostname",
|
"primaryHostname": "Primary Hostname",
|
||||||
@ -91,5 +91,25 @@
|
|||||||
"Allowed commands:": "Allowed commands:",
|
"Allowed commands:": "Allowed commands:",
|
||||||
"Internal Networks": "Internal Networks",
|
"Internal Networks": "Internal Networks",
|
||||||
"External Networks": "External Networks",
|
"External Networks": "External Networks",
|
||||||
"No External Networks": "No External Networks"
|
"No External Networks": "No External Networks",
|
||||||
|
"reverseProxyMsg1": "Using a Reverse Proxy?",
|
||||||
|
"reverseProxyMsg2": "Check how to config it for WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Cannot connect to the socket server.",
|
||||||
|
"reconnecting...": "Reconnecting…",
|
||||||
|
"connecting...": "Connecting to the socket server…",
|
||||||
|
"url": "URL | URLs",
|
||||||
|
"extra": "Extra",
|
||||||
|
"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?"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"languageName": "Francais",
|
"languageName": "Français",
|
||||||
"Create your admin account": "Créez votre compte administrateur",
|
"Create your admin account": "Créez votre compte administrateur",
|
||||||
"authIncorrectCreds": "identifiant ou mot de passe incorrect.",
|
"authIncorrectCreds": "identifiant ou mot de passe incorrect.",
|
||||||
"Repeat Password": "Répéter le mot de passe",
|
"Repeat Password": "Répéter le mot de passe",
|
||||||
@ -11,8 +11,8 @@
|
|||||||
"console": "Console",
|
"console": "Console",
|
||||||
"registry": "Registre",
|
"registry": "Registre",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Créez votre première pile!",
|
"addFirstStackMsg": "Créez votre première pile !",
|
||||||
"stackName" : "Nom de la pile",
|
"stackName": "Nom de la pile",
|
||||||
"deployStack": "Déployer",
|
"deployStack": "Déployer",
|
||||||
"deleteStack": "Supprimer",
|
"deleteStack": "Supprimer",
|
||||||
"stopStack": "Arrêter",
|
"stopStack": "Arrêter",
|
||||||
@ -22,11 +22,11 @@
|
|||||||
"editStack": "Modifier",
|
"editStack": "Modifier",
|
||||||
"discardStack": "Ignorer",
|
"discardStack": "Ignorer",
|
||||||
"saveStackDraft": "Sauvegarder",
|
"saveStackDraft": "Sauvegarder",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/A",
|
||||||
"deleteStackMsg": "Êtes-vous sûr de vouloir supprimer cette pile ?",
|
"deleteStackMsg": "Êtes-vous sûr de vouloir supprimer cette pile ?",
|
||||||
"stackNotManagedByDockgeMsg": "Cette pile n'est pas gérée par Dockge.",
|
"stackNotManagedByDockgeMsg": "Cette pile n'est pas gérée par Dockge.",
|
||||||
"primaryHostname": "Nom d'hôte principal",
|
"primaryHostname": "Nom d'hôte principal",
|
||||||
"general": "Générale",
|
"general": "Général",
|
||||||
"container": "Conteneur | Conteneurs",
|
"container": "Conteneur | Conteneurs",
|
||||||
"scanFolder": "Analyser le dossier des piles",
|
"scanFolder": "Analyser le dossier des piles",
|
||||||
"dockerImage": "Image",
|
"dockerImage": "Image",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"autoGet": "Obtention automatique",
|
"autoGet": "Obtention automatique",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"Edit": "Modifier",
|
"Edit": "Modifier",
|
||||||
"applyToYAML": "Appliquer à YAML",
|
"applyToYAML": "Appliquer au YAML",
|
||||||
"createExternalNetwork": "Créer",
|
"createExternalNetwork": "Créer",
|
||||||
"addInternalNetwork": "Ajouter",
|
"addInternalNetwork": "Ajouter",
|
||||||
"Save": "Enregistrer",
|
"Save": "Enregistrer",
|
||||||
@ -87,8 +87,17 @@
|
|||||||
"Appearance": "Apparence",
|
"Appearance": "Apparence",
|
||||||
"Security": "Sécurité",
|
"Security": "Sécurité",
|
||||||
"About": "À propos",
|
"About": "À propos",
|
||||||
"Allowed commands:": "Commandes autorisées:",
|
"Allowed commands:": "Commandes autorisées :",
|
||||||
"Internal Networks": "Réseaux Internes",
|
"Internal Networks": "Réseaux Internes",
|
||||||
"External Networks": "Réseaux Externes",
|
"External Networks": "Réseaux Externes",
|
||||||
"No External Networks": "Aucun Réseau Externe"
|
"No External Networks": "Aucun Réseau Externe",
|
||||||
|
"reverseProxyMsg2": "Vérifier comment le configurer pour WebSocket",
|
||||||
|
"connecting...": "Connexion au serveur socket…",
|
||||||
|
"url": "URL | URLs",
|
||||||
|
"extra": "Supplémentaire",
|
||||||
|
"downStack": "Arrêter et désactiver",
|
||||||
|
"reverseProxyMsg1": "Utilisez vous un proxy inverse ?",
|
||||||
|
"Cannot connect to the socket server.": "Impossible de se connecter au serveur socket.",
|
||||||
|
"reconnecting...": "Reconnexion…",
|
||||||
|
"newUpdate": "Nouvelle mise à jour"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"editStack": "Modifica",
|
"editStack": "Modifica",
|
||||||
"discardStack": "Annulla",
|
"discardStack": "Annulla",
|
||||||
"saveStackDraft": "Salva",
|
"saveStackDraft": "Salva",
|
||||||
"notAvailableShort" : "N/D",
|
"notAvailableShort": "N/D",
|
||||||
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
|
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
|
||||||
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
|
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
|
||||||
"primaryHostname": "Hostname primario",
|
"primaryHostname": "Hostname primario",
|
||||||
@ -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",
|
||||||
@ -91,5 +91,12 @@
|
|||||||
"Allowed commands:": "Comandi permessi:",
|
"Allowed commands:": "Comandi permessi:",
|
||||||
"Internal Networks": "Reti interne",
|
"Internal Networks": "Reti interne",
|
||||||
"External Networks": "Reti esterne",
|
"External Networks": "Reti esterne",
|
||||||
"No External Networks": "Nessuna rete esterna"
|
"No External Networks": "Nessuna rete esterna",
|
||||||
|
"reverseProxyMsg1": "Utilizzando un proxy inverso?",
|
||||||
|
"reverseProxyMsg2": "Controlla come configurarlo per WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Impossibile connettersi al server socket.",
|
||||||
|
"connecting...": "Connessione al server socket…",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Riconnessione…",
|
||||||
|
"url": "Indirizzo | Indirizzi"
|
||||||
}
|
}
|
||||||
|
98
frontend/src/lang/ja.json
Normal file
98
frontend/src/lang/ja.json
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"authIncorrectCreds": "ユーザーネームまたはパスワードが正しくありません。",
|
||||||
|
"PasswordsDoNotMatch": "パスワードが一致しません。",
|
||||||
|
"Repeat Password": "パスワードを再度入力してください",
|
||||||
|
"Create": "作成",
|
||||||
|
"signedInDispDisabled": "認証が無効化されています。",
|
||||||
|
"home": "ホーム",
|
||||||
|
"console": "コンソール",
|
||||||
|
"registry": "レジストリ",
|
||||||
|
"stackName": "スタック名",
|
||||||
|
"deployStack": "デプロイ",
|
||||||
|
"deleteStack": "削除",
|
||||||
|
"stopStack": "停止",
|
||||||
|
"restartStack": "再起動",
|
||||||
|
"updateStack": "更新",
|
||||||
|
"startStack": "起動",
|
||||||
|
"editStack": "編集",
|
||||||
|
"discardStack": "破棄",
|
||||||
|
"saveStackDraft": "保存",
|
||||||
|
"stackNotManagedByDockgeMsg": "このスタックはDockgeによって管理されていません。",
|
||||||
|
"general": "一般",
|
||||||
|
"scanFolder": "スタックフォルダをスキャン",
|
||||||
|
"dockerImage": "イメージ",
|
||||||
|
"environmentVariable": "環境変数",
|
||||||
|
"restartPolicy": "再起動ポリシー",
|
||||||
|
"containerName": "コンテナ名",
|
||||||
|
"port": "ポート",
|
||||||
|
"volume": "ボリューム",
|
||||||
|
"network": "ネットワーク",
|
||||||
|
"addListItem": "{0} を追加",
|
||||||
|
"addContainer": "コンテナを追加",
|
||||||
|
"addNetwork": "ネットワークを追加",
|
||||||
|
"compose": "Compose",
|
||||||
|
"primaryHostname": "主ホスト名",
|
||||||
|
"container": "コンテナ",
|
||||||
|
"dependsOn": "コンテナ依存関係",
|
||||||
|
"downStack": "停止して削除",
|
||||||
|
"notAvailableShort": "N/A",
|
||||||
|
"restartPolicyUnlessStopped": "手動で停止されるまで",
|
||||||
|
"restartPolicyAlways": "常時",
|
||||||
|
"restartPolicyOnFailure": "失敗時",
|
||||||
|
"restartPolicyNo": "しない",
|
||||||
|
"passwordNotMatchMsg": "繰り返しのパスワードが一致しません。",
|
||||||
|
"autoGet": "自動取得",
|
||||||
|
"add": "追加",
|
||||||
|
"Edit": "編集",
|
||||||
|
"applyToYAML": "YAMLに適用",
|
||||||
|
"createExternalNetwork": "作成",
|
||||||
|
"addInternalNetwork": "追加",
|
||||||
|
"Save": "保存",
|
||||||
|
"Language": "言語",
|
||||||
|
"Change Password": "パスワードを変更する",
|
||||||
|
"Current Password": "現在のパスワード",
|
||||||
|
"New Password": "新しいパスワード",
|
||||||
|
"Update Password": "パスワードを更新",
|
||||||
|
"Advanced": "高度",
|
||||||
|
"Please use this option carefully!": "このオプションは注意して使用してください!",
|
||||||
|
"Enable Auth": "認証を有効化",
|
||||||
|
"Disable Auth": "認証を無効化",
|
||||||
|
"Check Update On GitHub": "GitHubで更新を確認",
|
||||||
|
"Show update if available": "アップデートがある場合表示",
|
||||||
|
"Also check beta release": "ベータ版のリリースも確認する",
|
||||||
|
"Login": "ログイン",
|
||||||
|
"Username": "ユーザー名",
|
||||||
|
"Password": "パスワード",
|
||||||
|
"Settings": "設定",
|
||||||
|
"Logout": "ログアウト",
|
||||||
|
"Convert to Compose": "Composeに変換",
|
||||||
|
"Appearance": "外観",
|
||||||
|
"Security": "セキュリティ",
|
||||||
|
"Allowed commands:": "許可されたコマンド:",
|
||||||
|
"Internal Networks": "内部ネットワーク",
|
||||||
|
"External Networks": "外部ネットワーク",
|
||||||
|
"reverseProxyMsg2": "WebSocketの設定方法を確認",
|
||||||
|
"Cannot connect to the socket server.": "ソケットサーバーに接続できません。",
|
||||||
|
"reconnecting...": "再接続中…",
|
||||||
|
"Leave": "やめる",
|
||||||
|
"Frontend Version": "フロントエンドバージョン",
|
||||||
|
"Remember me": "覚えておく",
|
||||||
|
"No External Networks": "外部ネットワークなし",
|
||||||
|
"exited": "終了済み",
|
||||||
|
"inactive": "非アクティブ",
|
||||||
|
"active": "アクティブ",
|
||||||
|
"languageName": "日本語",
|
||||||
|
"Create your admin account": "管理者アカウントを作成してください",
|
||||||
|
"signedInDisp": "{0} としてログイン中",
|
||||||
|
"addFirstStackMsg": "最初のスタックを組み立てましょう!",
|
||||||
|
"deleteStackMsg": "本当にこのスタックを削除しますか?",
|
||||||
|
"deleteContainer": "削除",
|
||||||
|
"disableauth.message1": "本当に<strong>認証を無効化</strong>しますか?",
|
||||||
|
"disableauth.message2": "これはCloudflare AccessやAutheliaなどの認証手段をDockgeの前段に置いて<strong>サードパーティー認証を実装することをあなたが意図している</strong>場合のために設計されています。",
|
||||||
|
"Current User": "現在のユーザー",
|
||||||
|
"Repeat New Password": "新しいパスワードを繰り返してください",
|
||||||
|
"I understand, please disable": "理解しました。無効化してください",
|
||||||
|
"Lowercase only": "小文字のみ",
|
||||||
|
"reverseProxyMsg1": "リバースプロキシを使用していますか?",
|
||||||
|
"connecting...": "ソケットサーバーに接続中…"
|
||||||
|
}
|
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "허용된 명령어:",
|
"Allowed commands:": "허용된 명령어:",
|
||||||
"Internal Networks": "내부 네트워크",
|
"Internal Networks": "내부 네트워크",
|
||||||
"External Networks": "외부 네트워크",
|
"External Networks": "외부 네트워크",
|
||||||
"No External Networks": "외부 네트워크 없음"
|
"No External Networks": "외부 네트워크 없음",
|
||||||
|
"reverseProxyMsg2": "여기서 WebSocket을 위한 설정을 확인해 보세요",
|
||||||
|
"downStack": "정지 & Down",
|
||||||
|
"reverseProxyMsg1": "리버스 프록시를 사용하고 계신가요?",
|
||||||
|
"Cannot connect to the socket server.": "소켓 서버에 연결하지 못했습니다.",
|
||||||
|
"connecting...": "소켓 서버에 연결하는 중…",
|
||||||
|
"extra": "기타",
|
||||||
|
"url": "URL | URL",
|
||||||
|
"reconnecting...": "재연결 중…"
|
||||||
}
|
}
|
||||||
|
102
frontend/src/lang/nl.json
Normal file
102
frontend/src/lang/nl.json
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"languageName": "Nederlands",
|
||||||
|
"authIncorrectCreds": "Onjuiste gebruikersnaam of wachtwoord.",
|
||||||
|
"PasswordsDoNotMatch": "Paswoorden komen niet overeen.",
|
||||||
|
"Repeat Password": "Herhaal wachtwoord",
|
||||||
|
"Create": "Aanmaken",
|
||||||
|
"signedInDisp": "Ingelogd als {0}",
|
||||||
|
"home": "Startpagina",
|
||||||
|
"console": "Console",
|
||||||
|
"registry": "Register",
|
||||||
|
"compose": "Samenstellen",
|
||||||
|
"stackName": "Stack naam",
|
||||||
|
"deployStack": "Opzetten",
|
||||||
|
"deleteStack": "Verwijder",
|
||||||
|
"stopStack": "Stop",
|
||||||
|
"restartStack": "Herstart",
|
||||||
|
"updateStack": "Update",
|
||||||
|
"startStack": "Start",
|
||||||
|
"downStack": "Stop & Down",
|
||||||
|
"editStack": "Bewerken",
|
||||||
|
"discardStack": "Verwijderen",
|
||||||
|
"saveStackDraft": "Opslaan",
|
||||||
|
"notAvailableShort": "NVT",
|
||||||
|
"stackNotManagedByDockgeMsg": "Deze stack wordt niet beheerd door Dockge.",
|
||||||
|
"primaryHostname": "Primaire hostnaam",
|
||||||
|
"general": "Algemeen",
|
||||||
|
"scanFolder": "Scan stacks folder",
|
||||||
|
"dockerImage": "Image",
|
||||||
|
"restartPolicyUnlessStopped": "Tenzij gestopt",
|
||||||
|
"restartPolicyAlways": "Altijd",
|
||||||
|
"restartPolicyOnFailure": "Bij fout",
|
||||||
|
"restartPolicyNo": "Neen",
|
||||||
|
"environmentVariable": "Omgevings variabele(n)",
|
||||||
|
"restartPolicy": "Herstart policy",
|
||||||
|
"containerName": "Containernaam",
|
||||||
|
"port": "Poort(en)",
|
||||||
|
"volume": "Volume(s)",
|
||||||
|
"network": "Netwerk(en)",
|
||||||
|
"addListItem": "Voeg {0} toe",
|
||||||
|
"deleteContainer": "Verwijder",
|
||||||
|
"addContainer": "Container toevoegen",
|
||||||
|
"addNetwork": "Netwerk toevoegen",
|
||||||
|
"signedInDispDisabled": "Aanmelden uitgeschakeld.",
|
||||||
|
"container": "Container(s)",
|
||||||
|
"autoGet": "Auto ophalen",
|
||||||
|
"add": "Toevoegen",
|
||||||
|
"Edit": "Bewerken",
|
||||||
|
"applyToYAML": "Toevoegen aan YAML",
|
||||||
|
"createExternalNetwork": "Aanmaken",
|
||||||
|
"addInternalNetwork": "Toevoegen",
|
||||||
|
"Save": "Opslaan",
|
||||||
|
"Language": "Taal",
|
||||||
|
"Change Password": "Verander wachtwoord",
|
||||||
|
"Current Password": "Huidig wachtwoord",
|
||||||
|
"New Password": "Nieuw wachtwoord",
|
||||||
|
"Repeat New Password": "Herhaal nieuw wachtwoord",
|
||||||
|
"Update Password": "Update wachtwoord",
|
||||||
|
"Advanced": "Geavanceerd",
|
||||||
|
"I understand, please disable": "Begrepen, dit uitschakelen",
|
||||||
|
"Disable Auth": "Aanmelden uitschakelen",
|
||||||
|
"Enable Auth": "Aanmelden inschakelen",
|
||||||
|
"Leave": "Afmelden",
|
||||||
|
"Frontend Version": "Frontend versie",
|
||||||
|
"Check Update On GitHub": "Controleer via GitHub op updates",
|
||||||
|
"Show update if available": "Toon update indien beschikbaar",
|
||||||
|
"Remember me": "Onthoud mij",
|
||||||
|
"Login": "Inloggen",
|
||||||
|
"Username": "Gebruikersnaam",
|
||||||
|
"Password": "Wachtwoord",
|
||||||
|
"Settings": "Instellingen",
|
||||||
|
"Logout": "Uitloggen",
|
||||||
|
"Lowercase only": "Geen hoofdletters",
|
||||||
|
"Docker Run": "Docker run",
|
||||||
|
"active": "actief",
|
||||||
|
"exited": "gestopt",
|
||||||
|
"inactive": "inactief",
|
||||||
|
"Appearance": "Uiterlijk",
|
||||||
|
"Security": "Beveiliging",
|
||||||
|
"About": "Over",
|
||||||
|
"Allowed commands:": "Toegelaten commando's:",
|
||||||
|
"Internal Networks": "Interne netwerken",
|
||||||
|
"No External Networks": "Geen externe netwerken",
|
||||||
|
"reverseProxyMsg1": "Reverse proxy in gebruik?",
|
||||||
|
"reverseProxyMsg2": "Controleer hoe te configureren voor WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Kan geen verbinding maken met de socket server.",
|
||||||
|
"reconnecting...": "Herverbinden...",
|
||||||
|
"connecting...": "Verbinden met de socket server...",
|
||||||
|
"url": "Url(s)",
|
||||||
|
"extra": "Extra",
|
||||||
|
"Create your admin account": "Creëer je beheerders-account",
|
||||||
|
"addFirstStackMsg": "Maak je eerste stack!",
|
||||||
|
"deleteStackMsg": "Zeker dat je deze stack wilt verwijderen?",
|
||||||
|
"dependsOn": "Container afhankelijkheid | afhankelijkheden",
|
||||||
|
"disableauth.message1": "Zeker dat u <strong>aanmelden</strong> wilt uitschakelen?",
|
||||||
|
"disableauth.message2": "Dit is enkel bedoeld om te gebruiken wanneer je<strong> third-party autorisatie wilt gebruiken voor Dockge</strong>, zoals Cloudflare Acces, Authelia, ...",
|
||||||
|
"passwordNotMatchMsg": "De wachtwoorden komen niet overeen.",
|
||||||
|
"Current User": "Huidige gebruiker",
|
||||||
|
"Please use this option carefully!": "Wees voorzichtig met deze optie!",
|
||||||
|
"Also check beta release": "Controleer ook op beta releases",
|
||||||
|
"Convert to Compose": "Converteer naar compose",
|
||||||
|
"External Networks": "Externe netwerken"
|
||||||
|
}
|
@ -12,7 +12,7 @@
|
|||||||
"registry": "Registro",
|
"registry": "Registro",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Crie sua primeira stack!",
|
"addFirstStackMsg": "Crie sua primeira stack!",
|
||||||
"stackName" : "Nome da stack",
|
"stackName": "Nome da stack",
|
||||||
"deployStack": "Deploy",
|
"deployStack": "Deploy",
|
||||||
"deleteStack": "Excluir",
|
"deleteStack": "Excluir",
|
||||||
"stopStack": "Parar",
|
"stopStack": "Parar",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"editStack": "Editar",
|
"editStack": "Editar",
|
||||||
"discardStack": "Descartar",
|
"discardStack": "Descartar",
|
||||||
"saveStackDraft": "Salvar",
|
"saveStackDraft": "Salvar",
|
||||||
"notAvailableShort" : "N/D",
|
"notAvailableShort": "N/D",
|
||||||
"deleteStackMsg": "Tem certeza que deseja excluir esta stack?",
|
"deleteStackMsg": "Tem certeza que deseja excluir esta stack?",
|
||||||
"stackNotManagedByDockgeMsg": "Esta stack não é gerenciada pelo Dockge.",
|
"stackNotManagedByDockgeMsg": "Esta stack não é gerenciada pelo Dockge.",
|
||||||
"primaryHostname": "Nome do Host Primário",
|
"primaryHostname": "Nome do Host Primário",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Comandos permitidos:",
|
"Allowed commands:": "Comandos permitidos:",
|
||||||
"Internal Networks": "Redes internas",
|
"Internal Networks": "Redes internas",
|
||||||
"External Networks": "Redes externas",
|
"External Networks": "Redes externas",
|
||||||
"No External Networks": "Sem redes externas"
|
"No External Networks": "Sem redes externas",
|
||||||
|
"reverseProxyMsg2": "Veja como configurar para WebSocket",
|
||||||
|
"downStack": "Parar & Encerrar",
|
||||||
|
"reverseProxyMsg1": "Utiliza proxy reverso?",
|
||||||
|
"Cannot connect to the socket server.": "Não é possível conectar ao socket server.",
|
||||||
|
"connecting...": "Conectando ao socket server…",
|
||||||
|
"url": "URL | URLs",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Reconectando…"
|
||||||
}
|
}
|
||||||
|
102
frontend/src/lang/ro.json
Normal file
102
frontend/src/lang/ro.json
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"Create your admin account": "Creați-vă contul de administrator",
|
||||||
|
"PasswordsDoNotMatch": "Parolele nu se potrivesc.",
|
||||||
|
"Repeat Password": "Repetați parola",
|
||||||
|
"signedInDisp": "Conectat ca {0}",
|
||||||
|
"signedInDispDisabled": "Autentificare dezactivată.",
|
||||||
|
"Create": "Creează",
|
||||||
|
"home": "Acasă",
|
||||||
|
"console": "Consolă",
|
||||||
|
"registry": "Registru",
|
||||||
|
"compose": "Compune",
|
||||||
|
"addFirstStackMsg": "Compune prima ta stivă!",
|
||||||
|
"stackName": "Nume stivă",
|
||||||
|
"deployStack": "Lansează",
|
||||||
|
"deleteStack": "Șterge",
|
||||||
|
"stopStack": "Oprește",
|
||||||
|
"restartStack": "Repornire",
|
||||||
|
"updateStack": "Actualizare",
|
||||||
|
"languageName": "Română",
|
||||||
|
"authIncorrectCreds": "Numele de utilizator sau parola incorectă.",
|
||||||
|
"startStack": "Pornește",
|
||||||
|
"editStack": "Editați",
|
||||||
|
"discardStack": "Renunţa",
|
||||||
|
"notAvailableShort": "N/A",
|
||||||
|
"deleteStackMsg": "Sigur doriți să ștergeți această stivă?",
|
||||||
|
"stackNotManagedByDockgeMsg": "Această stivă nu este gestionată de Dockge.",
|
||||||
|
"primaryHostname": "Numele gazdei principale",
|
||||||
|
"general": "General",
|
||||||
|
"container": "Container | Containere",
|
||||||
|
"scanFolder": "Scanează folderul cu stive",
|
||||||
|
"dockerImage": "Imagine",
|
||||||
|
"restartPolicyOnFailure": "La Defecţiune",
|
||||||
|
"restartPolicyNo": "Nu",
|
||||||
|
"restartPolicy": "Politica de repornire",
|
||||||
|
"restartPolicyAlways": "Mereu",
|
||||||
|
"containerName": "Numele Containerului",
|
||||||
|
"port": "Port | Porturi",
|
||||||
|
"volume": "Volum | Volume",
|
||||||
|
"network": "Reţea | Reţele",
|
||||||
|
"dependsOn": "Dependența containerului | Dependențele containerelor",
|
||||||
|
"addListItem": "Adaugă {0}",
|
||||||
|
"deleteContainer": "Șterge",
|
||||||
|
"addContainer": "Adaugă Container",
|
||||||
|
"addNetwork": "Adaugă Rețea",
|
||||||
|
"addInternalNetwork": "Adaugă",
|
||||||
|
"Save": "Salvează",
|
||||||
|
"Current User": "Utilizator Curent",
|
||||||
|
"Change Password": "Schimbă Parola",
|
||||||
|
"Current Password": "Parolă Curenta",
|
||||||
|
"New Password": "Parolă Nouă",
|
||||||
|
"Repeat New Password": "Repetă Parola Nouă",
|
||||||
|
"Update Password": "Actualizează Parola",
|
||||||
|
"Advanced": "Avansat",
|
||||||
|
"Enable Auth": "Activați Autentificarea",
|
||||||
|
"Disable Auth": "Dezactivați Autentificarea",
|
||||||
|
"I understand, please disable": "Am înțeles, vă rog dezactivați",
|
||||||
|
"Leave": "Părăsiți",
|
||||||
|
"Frontend Version": "Versiunea Frontend",
|
||||||
|
"Check Update On GitHub": "Verificați actualizarea pe GitHub",
|
||||||
|
"Also check beta release": "Verificați și versiunea beta",
|
||||||
|
"Remember me": "Ține-mă minte",
|
||||||
|
"Login": "Autentificare",
|
||||||
|
"Username": "Nume de utilizator",
|
||||||
|
"Password": "Parolă",
|
||||||
|
"passwordNotMatchMsg": "Parola repetată nu se potrivește.",
|
||||||
|
"autoGet": "Obținere automată",
|
||||||
|
"add": "Adăuga",
|
||||||
|
"Edit": "Editați",
|
||||||
|
"applyToYAML": "Aplicați la YAML",
|
||||||
|
"createExternalNetwork": "Creează",
|
||||||
|
"Settings": "Setări",
|
||||||
|
"Logout": "Deconectare",
|
||||||
|
"Lowercase only": "Doar litere mici",
|
||||||
|
"Convert to Compose": "Convertiți în Compose",
|
||||||
|
"Docker Run": "Docker Run",
|
||||||
|
"active": "activ",
|
||||||
|
"exited": "ieșit",
|
||||||
|
"inactive": "inactiv",
|
||||||
|
"Appearance": "Aspect",
|
||||||
|
"Security": "Securitate",
|
||||||
|
"About": "Despre",
|
||||||
|
"Allowed commands:": "Comenzi permise:",
|
||||||
|
"Internal Networks": "Rețele interne",
|
||||||
|
"External Networks": "Rețele externe",
|
||||||
|
"No External Networks": "Fără rețele externe",
|
||||||
|
"reverseProxyMsg1": "Folosești un proxy invers?",
|
||||||
|
"reverseProxyMsg2": "Verificați cum să-l configurați pentru WebSocket",
|
||||||
|
"Cannot connect to the socket server.": "Nu se poate conecta la serverul socket.",
|
||||||
|
"reconnecting...": "Reconectare...",
|
||||||
|
"connecting...": "Se conectează la serverul socket...",
|
||||||
|
"url": "URL | URLs",
|
||||||
|
"extra": "Suplimentar",
|
||||||
|
"downStack": "Opriți & Coborâți",
|
||||||
|
"saveStackDraft": "Salvați",
|
||||||
|
"restartPolicyUnlessStopped": "Dacă nu este oprit",
|
||||||
|
"environmentVariable": "Variabila de mediu | Variabile de mediu",
|
||||||
|
"Language": "Limbă",
|
||||||
|
"Please use this option carefully!": "Vă rugăm să utilizați această opțiune cu atenție!",
|
||||||
|
"Show update if available": "Afișează actualizarea dacă este disponibilă",
|
||||||
|
"disableauth.message1": "Sigur doriți să <strong>dezactivați autentificarea</strong>?",
|
||||||
|
"disableauth.message2": "Este conceput pentru scenarii <strong>în care intenționați să implementați autentificarea terță</strong> în fața Dockge-lui, cum ar fi Cloudflare Access, Authelia sau alte mecanisme de autentificare."
|
||||||
|
}
|
@ -5,14 +5,14 @@
|
|||||||
"PasswordsDoNotMatch": "Пароль не совпадает.",
|
"PasswordsDoNotMatch": "Пароль не совпадает.",
|
||||||
"Repeat Password": "Повторите пароль",
|
"Repeat Password": "Повторите пароль",
|
||||||
"Create": "Создать",
|
"Create": "Создать",
|
||||||
"signedInDisp": "Авторизлван как {0}",
|
"signedInDisp": "Авторизован как {0}",
|
||||||
"signedInDispDisabled": "Авторизация выключена.",
|
"signedInDispDisabled": "Авторизация выключена.",
|
||||||
"home": "Главная",
|
"home": "Главная",
|
||||||
"console": "Консоль",
|
"console": "Консоль",
|
||||||
"registry": "Registry",
|
"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" : "Н/Д",
|
"notAvailableShort": "Н/Д",
|
||||||
"deleteStackMsg": "Вы уверены что хотите удалить этот стек?",
|
"deleteStackMsg": "Вы уверены что хотите удалить этот стек?",
|
||||||
"stackNotManagedByDockgeMsg": "Данный стек не обслуживается Dockge.",
|
"stackNotManagedByDockgeMsg": "Данный стек не обслуживается Dockge.",
|
||||||
"primaryHostname": "Имя хоста",
|
"primaryHostname": "Имя хоста",
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"Settings": "Настройки",
|
"Settings": "Настройки",
|
||||||
"Logout": "Выйти",
|
"Logout": "Выйти",
|
||||||
"Lowercase only": "Только нижний регистр",
|
"Lowercase only": "Только нижний регистр",
|
||||||
"Convert to Compose": "Преобразовать вCompose",
|
"Convert to Compose": "Преобразовать в Compose",
|
||||||
"Docker Run": "Запустить Docker",
|
"Docker Run": "Запустить Docker",
|
||||||
"active": "активный",
|
"active": "активный",
|
||||||
"exited": "завершенный",
|
"exited": "завершенный",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Разрешенные команды:",
|
"Allowed commands:": "Разрешенные команды:",
|
||||||
"Internal Networks": "Внутренние сети",
|
"Internal Networks": "Внутренние сети",
|
||||||
"External Networks": "Внешние сети",
|
"External Networks": "Внешние сети",
|
||||||
"No External Networks": "Нет внешних сетей"
|
"No External Networks": "Нет внешних сетей",
|
||||||
|
"downStack": "Остановить и выключить",
|
||||||
|
"reverseProxyMsg1": "Использовать Реверс Прокси?",
|
||||||
|
"reconnecting...": "Переподключение…",
|
||||||
|
"Cannot connect to the socket server.": "Не удается подключиться к серверу сокетов.",
|
||||||
|
"url": "URL адрес(а)",
|
||||||
|
"extra": "Дополнительно",
|
||||||
|
"reverseProxyMsg2": "Проверьте, как настроить его для WebSocket",
|
||||||
|
"connecting...": "Подключение к серверу сокетов…"
|
||||||
}
|
}
|
||||||
|
@ -90,5 +90,10 @@
|
|||||||
"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 …"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"languageName": "Svenska",
|
"languageName": "Svenska",
|
||||||
"Create your admin account": "Skapa ditt Admin-konto.",
|
"Create your admin account": "Skapa ditt Admin-konto",
|
||||||
"authIncorrectCreds": "Fel användarnamn eller lösenord.",
|
"authIncorrectCreds": "Fel användarnamn eller lösenord.",
|
||||||
"PasswordsDoNotMatch": "Lösenorden matchar inte.",
|
"PasswordsDoNotMatch": "Lösenorden matchar inte.",
|
||||||
"Repeat Password": "Repetera lösenord",
|
"Repeat Password": "Repetera lösenord",
|
||||||
@ -12,28 +12,28 @@
|
|||||||
"registry": "Register",
|
"registry": "Register",
|
||||||
"compose": "Komponera",
|
"compose": "Komponera",
|
||||||
"addFirstStackMsg": "Komponera din första stack!",
|
"addFirstStackMsg": "Komponera din första stack!",
|
||||||
"stackName" : "Stacknamn",
|
"stackName": "Stacknamn",
|
||||||
"deployStack": "Distribuera",
|
"deployStack": "Distribuera",
|
||||||
"deleteStack": "Radera",
|
"deleteStack": "Radera",
|
||||||
"stopStack": "Stop",
|
"stopStack": "Stoppa",
|
||||||
"restartStack": "Starta om",
|
"restartStack": "Starta om",
|
||||||
"updateStack": "Uppdatera",
|
"updateStack": "Uppdatera",
|
||||||
"startStack": "Starta",
|
"startStack": "Starta",
|
||||||
"downStack": "Stop & Ner",
|
"downStack": "Stoppa & Ner",
|
||||||
"editStack": "Redigera",
|
"editStack": "Redigera",
|
||||||
"discardStack": "Kasta",
|
"discardStack": "Kasta",
|
||||||
"saveStackDraft": "Spara",
|
"saveStackDraft": "Spara",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/A",
|
||||||
"deleteStackMsg": "Är du säker på att du vill radera stacken?",
|
"deleteStackMsg": "Är du säker på att du vill radera stacken?",
|
||||||
"stackNotManagedByDockgeMsg": "Denna stacken hanteras inte av Dockge.",
|
"stackNotManagedByDockgeMsg": "Denna stacken hanteras inte av Dockge.",
|
||||||
"primaryHostname": "Primärt värdnamn",
|
"primaryHostname": "Primärt värdnamn",
|
||||||
"general": "Allmän",
|
"general": "Allmän",
|
||||||
"container": "Container | Containrar",
|
"container": "Container | Containrar",
|
||||||
"scanFolder": "Scanna Stackfolder",
|
"scanFolder": "Skanna Stackmapp",
|
||||||
"dockerImage": "Bild",
|
"dockerImage": "Avbild",
|
||||||
"restartPolicyUnlessStopped": "Om inte stoppas",
|
"restartPolicyUnlessStopped": "Om inte stoppad",
|
||||||
"restartPolicyAlways": "Alltid",
|
"restartPolicyAlways": "Alltid",
|
||||||
"restartPolicyOnFailure": "Vid Misslyckande",
|
"restartPolicyOnFailure": "Vid misslyckande",
|
||||||
"restartPolicyNo": "Nej",
|
"restartPolicyNo": "Nej",
|
||||||
"environmentVariable": "Miljövariabel | Miljövariabler",
|
"environmentVariable": "Miljövariabel | Miljövariabler",
|
||||||
"restartPolicy": "Omstartspolicy",
|
"restartPolicy": "Omstartspolicy",
|
||||||
@ -44,12 +44,12 @@
|
|||||||
"dependsOn": "Containerberoende | Containerberoenden",
|
"dependsOn": "Containerberoende | Containerberoenden",
|
||||||
"addListItem": "Lägg till {0}",
|
"addListItem": "Lägg till {0}",
|
||||||
"deleteContainer": "Radera",
|
"deleteContainer": "Radera",
|
||||||
"addContainer": "Lägg till Container",
|
"addContainer": "Lägg till container",
|
||||||
"addNetwork": "Lägg till Nätverk",
|
"addNetwork": "Lägg till nätverk",
|
||||||
"disableauth.message1": "Är du säker på att du vill <strong>inaktivera autentisering</strong>?",
|
"disableauth.message1": "Är du säker på att du vill <strong>inaktivera autentisering</strong>?",
|
||||||
"disableauth.message2": "Det är designat för senarion <stong>när du ska implementera tredjeparts autentisering</strong> framör Dockge som Cloudflare Access, Authelia eller andra autentiseringsmekanismer.",
|
"disableauth.message2": "Det är designat för scenarion <strong>där du ska implementera tredjepartsautentisering</strong> framför Dockge som Cloudflare Access, Authelia eller andra autentiseringsmekanismer.",
|
||||||
"passwordNotMatchMsg": "Det upprepade lösenordet matchar inte",
|
"passwordNotMatchMsg": "Det upprepade lösenordet matchar inte.",
|
||||||
"autoGet": "Auto Hämta",
|
"autoGet": "Auto-hämta",
|
||||||
"add": "Lägg till",
|
"add": "Lägg till",
|
||||||
"Edit": "Redigera",
|
"Edit": "Redigera",
|
||||||
"applyToYAML": "Lägg till i YAML",
|
"applyToYAML": "Lägg till i YAML",
|
||||||
@ -57,8 +57,8 @@
|
|||||||
"addInternalNetwork": "Lägg till",
|
"addInternalNetwork": "Lägg till",
|
||||||
"Save": "Spara",
|
"Save": "Spara",
|
||||||
"Language": "Språk",
|
"Language": "Språk",
|
||||||
"Current User": "Nuvarande användaren",
|
"Current User": "Nuvarande användare",
|
||||||
"Change Password": "Byt lösenord",
|
"Change Password": "Ändra lösenord",
|
||||||
"Current Password": "Nuvarande lösenord",
|
"Current Password": "Nuvarande lösenord",
|
||||||
"New Password": "Nytt lösenord",
|
"New Password": "Nytt lösenord",
|
||||||
"Repeat New Password": "Upprepa nytt lösenord",
|
"Repeat New Password": "Upprepa nytt lösenord",
|
||||||
@ -70,9 +70,9 @@
|
|||||||
"I understand, please disable": "Jag förstår, vänligen inaktivera",
|
"I understand, please disable": "Jag förstår, vänligen inaktivera",
|
||||||
"Leave": "Lämna",
|
"Leave": "Lämna",
|
||||||
"Frontend Version": "Frontendversion",
|
"Frontend Version": "Frontendversion",
|
||||||
"Check Update On GitHub": "Kontrollera Uppdatering på GitHub",
|
"Check Update On GitHub": "Kontrollera uppdatering på GitHub",
|
||||||
"Show update if available": "Visa uppdatering om tillgänglig",
|
"Show update if available": "Visa uppdatering om tillgänglig",
|
||||||
"Also check beta release": "Kontrollera även betaversionen",
|
"Also check beta release": "Kontrollera även betaversioner",
|
||||||
"Remember me": "Kom ihåg mig",
|
"Remember me": "Kom ihåg mig",
|
||||||
"Login": "Logga in",
|
"Login": "Logga in",
|
||||||
"Username": "Användarnamn",
|
"Username": "Användarnamn",
|
||||||
@ -80,8 +80,8 @@
|
|||||||
"Settings": "Inställningar",
|
"Settings": "Inställningar",
|
||||||
"Logout": "Logga ut",
|
"Logout": "Logga ut",
|
||||||
"Lowercase only": "Endast små tecken",
|
"Lowercase only": "Endast små tecken",
|
||||||
"Convert to Compose": "Omvandla till Compose",
|
"Convert to Compose": "Omvandla till compose",
|
||||||
"Docker Run": "Docker Run",
|
"Docker Run": "Docker kör",
|
||||||
"active": "aktiv",
|
"active": "aktiv",
|
||||||
"exited": "avslutad",
|
"exited": "avslutad",
|
||||||
"inactive": "inaktiv",
|
"inactive": "inaktiv",
|
||||||
@ -89,7 +89,14 @@
|
|||||||
"Security": "Säkerhet",
|
"Security": "Säkerhet",
|
||||||
"About": "Om",
|
"About": "Om",
|
||||||
"Allowed commands:": "Tillåtna kommandon:",
|
"Allowed commands:": "Tillåtna kommandon:",
|
||||||
"Internal Networks": "Interna Nätverk",
|
"Internal Networks": "Interna nätverk",
|
||||||
"External Networks": "Externa Nätverk",
|
"External Networks": "Externa nätverk",
|
||||||
"No External Networks": "Inga Externa Nätverk"
|
"No External Networks": "Inga externa nätverk",
|
||||||
|
"reverseProxyMsg1": "Används omvänd proxy?",
|
||||||
|
"connecting...": "Ansluter till socketserver…",
|
||||||
|
"Cannot connect to the socket server.": "Kan inte ansluta till socketservern.",
|
||||||
|
"reverseProxyMsg2": "Kontrollera hur man konfigurerar webbsocket",
|
||||||
|
"url": "URL | URLer",
|
||||||
|
"extra": "Extra",
|
||||||
|
"reconnecting...": "Återansluter…"
|
||||||
}
|
}
|
||||||
|
@ -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...": "กำลังเชื่อมต่อใหม่…"
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"registry": "Kayıt Defteri",
|
"registry": "Kayıt Defteri",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "İlk yığınınızı oluşturun!",
|
"addFirstStackMsg": "İlk yığınınızı oluşturun!",
|
||||||
"stackName" : "Yığın Adı",
|
"stackName": "Yığın Adı",
|
||||||
"deployStack": "Dağıtmak",
|
"deployStack": "Dağıtmak",
|
||||||
"deleteStack": "Sil",
|
"deleteStack": "Sil",
|
||||||
"stopStack": "Dudur",
|
"stopStack": "Dudur",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"editStack": "Düzenle",
|
"editStack": "Düzenle",
|
||||||
"discardStack": "Çıkar",
|
"discardStack": "Çıkar",
|
||||||
"saveStackDraft": "Kaydet",
|
"saveStackDraft": "Kaydet",
|
||||||
"notAvailableShort" : "N/A",
|
"notAvailableShort": "N/A",
|
||||||
"deleteStackMsg": "Bu yığını silmek istediğinizden emin misiniz?",
|
"deleteStackMsg": "Bu yığını silmek istediğinizden emin misiniz?",
|
||||||
"stackNotManagedByDockgeMsg": "Bu yığın Dockge tarafından yönetilmemektedir.",
|
"stackNotManagedByDockgeMsg": "Bu yığın Dockge tarafından yönetilmemektedir.",
|
||||||
"primaryHostname": "Birincil Ana Bilgisayar Adı",
|
"primaryHostname": "Birincil Ana Bilgisayar Adı",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "İzin verilen komutlar:",
|
"Allowed commands:": "İzin verilen komutlar:",
|
||||||
"Internal Networks": "İç Ağlar",
|
"Internal Networks": "İç Ağlar",
|
||||||
"External Networks": "Dış Ağlar",
|
"External Networks": "Dış Ağlar",
|
||||||
"No External Networks": "Dış Ağ Yok"
|
"No External Networks": "Dış Ağ Yok",
|
||||||
|
"extra": "Ekstra",
|
||||||
|
"reverseProxyMsg1": "Ters Proxy mi kullanıyorsunuz?",
|
||||||
|
"reverseProxyMsg2": "WebSocket için nasıl yapılandırma yapılacağını kontrol edin",
|
||||||
|
"reconnecting...": "Yeniden bağlanıyor…",
|
||||||
|
"connecting...": "Soket sunucusuna bağlanıyor…",
|
||||||
|
"url": "URL | URL’ler",
|
||||||
|
"Cannot connect to the socket server.": "Soket sunucusuna bağlanılamıyor.",
|
||||||
|
"downStack": "Durdur & Kapat"
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"languageName": "Українська",
|
"languageName": "Українська",
|
||||||
"Create your admin account": "Створити акаунт адміністратора",
|
"Create your admin account": "Створити акаунт адміністратора",
|
||||||
"authIncorrectCreds": "Невірний логін чи пароль",
|
"authIncorrectCreds": "Неправильне ім'я користувача або пароль.",
|
||||||
"PasswordsDoNotMatch": "Паролі не співпадають.",
|
"PasswordsDoNotMatch": "Паролі не збігаються.",
|
||||||
"Repeat Password": "Повторіть пароль",
|
"Repeat Password": "Повторіть пароль",
|
||||||
"Create": "Створити",
|
"Create": "Створити",
|
||||||
"signedInDisp": "Авторизовано як {0}",
|
"signedInDisp": "Авторизовано як {0}",
|
||||||
@ -12,8 +12,8 @@
|
|||||||
"registry": "Registry",
|
"registry": "Registry",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "Додайте свій перший стек!",
|
"addFirstStackMsg": "Додайте свій перший стек!",
|
||||||
"stackName" : "Назва стеку",
|
"stackName": "Назва стеку",
|
||||||
"deployStack": "Розрознути",
|
"deployStack": "Розгорнути",
|
||||||
"deleteStack": "Видалити",
|
"deleteStack": "Видалити",
|
||||||
"stopStack": "Зупинити",
|
"stopStack": "Зупинити",
|
||||||
"restartStack": "Перезапустити",
|
"restartStack": "Перезапустити",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"editStack": "Редагувати",
|
"editStack": "Редагувати",
|
||||||
"discardStack": "Відмінити",
|
"discardStack": "Відмінити",
|
||||||
"saveStackDraft": "Зберегти",
|
"saveStackDraft": "Зберегти",
|
||||||
"notAvailableShort" : "Н/Д",
|
"notAvailableShort": "Н/Д",
|
||||||
"deleteStackMsg": "Ви впевнені що хочете видалити цей стек?",
|
"deleteStackMsg": "Ви впевнені що хочете видалити цей стек?",
|
||||||
"stackNotManagedByDockgeMsg": "Даний стек не управляється Dockge.",
|
"stackNotManagedByDockgeMsg": "Даний стек не управляється Dockge.",
|
||||||
"primaryHostname": "Назва хосту",
|
"primaryHostname": "Назва хосту",
|
||||||
@ -47,7 +47,7 @@
|
|||||||
"addNetwork": "Додати Мережу",
|
"addNetwork": "Додати Мережу",
|
||||||
"disableauth.message1": "Ви впевнені що хочете <strong>вимкнути авторизацію</strong>?",
|
"disableauth.message1": "Ви впевнені що хочете <strong>вимкнути авторизацію</strong>?",
|
||||||
"disableauth.message2": "Це призначено для сценаріїв, <strong>де ви збираєтесь використати сторонню авторизацію</strong> перед Dockge, наприклад Cloudflare Access, Authelia чи інші.",
|
"disableauth.message2": "Це призначено для сценаріїв, <strong>де ви збираєтесь використати сторонню авторизацію</strong> перед Dockge, наприклад Cloudflare Access, Authelia чи інші.",
|
||||||
"passwordNotMatchMsg": "Паролі не співпадають.",
|
"passwordNotMatchMsg": "Повторення паролю не збігається.",
|
||||||
"autoGet": "Отримати",
|
"autoGet": "Отримати",
|
||||||
"add": "Додати",
|
"add": "Додати",
|
||||||
"Edit": "Змінити",
|
"Edit": "Змінити",
|
||||||
@ -63,10 +63,10 @@
|
|||||||
"Repeat New Password": "Повторіть новий пароль",
|
"Repeat New Password": "Повторіть новий пароль",
|
||||||
"Update Password": "Оновити пароль",
|
"Update Password": "Оновити пароль",
|
||||||
"Advanced": "Розширені опції",
|
"Advanced": "Розширені опції",
|
||||||
"Please use this option carefully!": "Будь-ласка, використовуйте цю опцію з обережністю!",
|
"Please use this option carefully!": "Будь ласка, використовуйте цю опцію з обережністю!",
|
||||||
"Enable Auth": "Увімкнути автентифікацію",
|
"Enable Auth": "Увімкнути автентифікацію",
|
||||||
"Disable Auth": "Вимкнути автентифікацію",
|
"Disable Auth": "Вимкнути автентифікацію",
|
||||||
"I understand, please disable": "Зрозуміло, все-одно вимкнути",
|
"I understand, please disable": "Зрозуміло, все одно вимкнути",
|
||||||
"Leave": "Покинути",
|
"Leave": "Покинути",
|
||||||
"Frontend Version": "Версія інтерфейсу",
|
"Frontend Version": "Версія інтерфейсу",
|
||||||
"Check Update On GitHub": "Перевірити оновлення на GitHub",
|
"Check Update On GitHub": "Перевірити оновлення на GitHub",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "Дозволені команди:",
|
"Allowed commands:": "Дозволені команди:",
|
||||||
"Internal Networks": "Внутрішні мережі",
|
"Internal Networks": "Внутрішні мережі",
|
||||||
"External Networks": "Зовнішні мережі",
|
"External Networks": "Зовнішні мережі",
|
||||||
"No External Networks": "Немає зовнішніх мереж"
|
"No External Networks": "Немає зовнішніх мереж",
|
||||||
|
"downStack": "Зупинити і вимкнути",
|
||||||
|
"reverseProxyMsg1": "Використовуєте зворотній проксі?",
|
||||||
|
"Cannot connect to the socket server.": "Не вдається підключитися до сервера сокетів.",
|
||||||
|
"reconnecting...": "Повторне підключення…",
|
||||||
|
"connecting...": "Підключення до сервера сокетів…",
|
||||||
|
"url": "URL-адреса | URL-адреси",
|
||||||
|
"reverseProxyMsg2": "Перевірте, як налаштувати його для WebSocket",
|
||||||
|
"extra": "Додатково"
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,23 @@
|
|||||||
"registry": "رجسٹری",
|
"registry": "رجسٹری",
|
||||||
"compose": "تحریر",
|
"compose": "تحریر",
|
||||||
"addFirstStackMsg": "اپنا پہلا اسٹیک کمپوز کریں!",
|
"addFirstStackMsg": "اپنا پہلا اسٹیک کمپوز کریں!",
|
||||||
"stackName" : "اسٹیک کا نام",
|
"stackName": "اسٹیک کا نام",
|
||||||
"deployStack": "تعینات",
|
"deployStack": "تعینات",
|
||||||
"deleteStack": "حذف کریں",
|
"deleteStack": "حذف کریں",
|
||||||
"stopStack": "روکو",
|
"stopStack": "روکو",
|
||||||
"restartStack": "دوبارہ شروع کریں",
|
"restartStack": "دوبارہ شروع کریں",
|
||||||
"updateStack": "اپ ڈیٹ",
|
"updateStack": "اپ ڈیٹ",
|
||||||
"startStack": "شروع کریں۔",
|
"startStack": "شروع کریں",
|
||||||
"editStack": "ترمیم",
|
"editStack": "ترمیم",
|
||||||
"discardStack": "رد کر دیں۔",
|
"discardStack": "رد کر دیں",
|
||||||
"saveStackDraft": "محفوظ کریں۔",
|
"saveStackDraft": "محفوظ کریں۔",
|
||||||
"notAvailableShort" : "N / A",
|
"notAvailableShort": "N / A",
|
||||||
"deleteStackMsg": "کیا آپ واقعی اس اسٹیک کو حذف کرنا چاہتے ہیں؟",
|
"deleteStackMsg": "کیا آپ واقعی اس اسٹیک کو حذف کرنا چاہتے ہیں؟",
|
||||||
"stackNotManagedByDockgeMsg": "یہ اسٹیک Dockge کے زیر انتظام نہیں ہے۔",
|
"stackNotManagedByDockgeMsg": "یہ اسٹیک Dockge کے زیر انتظام نہیں ہے۔",
|
||||||
"primaryHostname": "بنیادی میزبان نام",
|
"primaryHostname": "بنیادی میزبان نام",
|
||||||
"general": "جنرل",
|
"general": "جنرل",
|
||||||
"container": "کنٹینر | کنٹینرز",
|
"container": "کنٹینر | کنٹینرز",
|
||||||
"scanFolder": "اسٹیک فولڈر کو اسکین کریں۔",
|
"scanFolder": "اسٹیک فولڈر کو اسکین کریں",
|
||||||
"dockerImage": "تصویر",
|
"dockerImage": "تصویر",
|
||||||
"restartPolicyUnlessStopped": "جب تک روکا نہیں جاتا",
|
"restartPolicyUnlessStopped": "جب تک روکا نہیں جاتا",
|
||||||
"restartPolicyAlways": "ہمیشہ",
|
"restartPolicyAlways": "ہمیشہ",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"autoGet": "آٹو حاصل کریں",
|
"autoGet": "آٹو حاصل کریں",
|
||||||
"add": "شامل کریں",
|
"add": "شامل کریں",
|
||||||
"Edit": "ترمیم",
|
"Edit": "ترمیم",
|
||||||
"applyToYAML": "YAML پر درخواست دیں۔",
|
"applyToYAML": "YAML پر درخواست دیں",
|
||||||
"createExternalNetwork": "بنانا",
|
"createExternalNetwork": "بنانا",
|
||||||
"addInternalNetwork": "شامل کریں",
|
"addInternalNetwork": "شامل کریں",
|
||||||
"Save": "محفوظ کریں",
|
"Save": "محفوظ کریں",
|
||||||
@ -64,12 +64,12 @@
|
|||||||
"Update Password": "پاس ورڈ اپ ڈیٹ کریں",
|
"Update Password": "پاس ورڈ اپ ڈیٹ کریں",
|
||||||
"Advanced": "ترقی یافتہ",
|
"Advanced": "ترقی یافتہ",
|
||||||
"Please use this option carefully!": "براہ کرم اس اختیار کو احتیاط سے استعمال کریں!",
|
"Please use this option carefully!": "براہ کرم اس اختیار کو احتیاط سے استعمال کریں!",
|
||||||
"Enable Auth": "تصدیق کو فعال کریں۔",
|
"Enable Auth": "تصدیق کو فعال کریں",
|
||||||
"Disable Auth": "توثیق کو غیر فعال کریں۔",
|
"Disable Auth": "توثیق کو غیر فعال کریں",
|
||||||
"I understand, please disable": "میں سمجھتا ہوں، براہ کرم غیر فعال کریں۔",
|
"I understand, please disable": "میں سمجھتا ہوں، براہ کرم غیر فعال کریں",
|
||||||
"Leave": "چھوڑ دو",
|
"Leave": "چھوڑ دو",
|
||||||
"Frontend Version": "فرنٹ اینڈ ورژن",
|
"Frontend Version": "فرنٹ اینڈ ورژن",
|
||||||
"Check Update On GitHub": "گیتوب پر اپ ڈیٹ چیک کریں۔",
|
"Check Update On GitHub": "گیتوب پر اپ ڈیٹ چیک کریں",
|
||||||
"Show update if available": "اگر دستیاب ہو تو اپ ڈیٹ دکھائیں",
|
"Show update if available": "اگر دستیاب ہو تو اپ ڈیٹ دکھائیں",
|
||||||
"Also check beta release": "بیٹا ریلیز بھی چیک کریں",
|
"Also check beta release": "بیٹا ریلیز بھی چیک کریں",
|
||||||
"Remember me": "مجھے پہچانتے ہو",
|
"Remember me": "مجھے پہچانتے ہو",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "اجازت شدہ احکامات:",
|
"Allowed commands:": "اجازت شدہ احکامات:",
|
||||||
"Internal Networks": "اندرونی نیٹ ورکس",
|
"Internal Networks": "اندرونی نیٹ ورکس",
|
||||||
"External Networks": "بیرونی نیٹ ورکس",
|
"External Networks": "بیرونی نیٹ ورکس",
|
||||||
"No External Networks": "کوئی بیرونی نیٹ ورک نہیں"
|
"No External Networks": "کوئی بیرونی نیٹ ورک نہیں",
|
||||||
|
"reverseProxyMsg1": "ایک ریورس پراکسی کا استعمال کرتے ہوئے؟",
|
||||||
|
"Cannot connect to the socket server.": "ساکٹ سرور سے منسلک نہیں ہو سکتا۔",
|
||||||
|
"reconnecting...": "دوبارہ منسلک ہو رہا ہے…",
|
||||||
|
"connecting...": "ساکٹ سرور سے منسلک ہو رہا ہے…",
|
||||||
|
"url": "یو آر ایل | یو آر ایل",
|
||||||
|
"extra": "اضافی",
|
||||||
|
"downStack": "اسٹاپ اینڈ ڈاؤن",
|
||||||
|
"reverseProxyMsg2": "اسے WebSocket کے لیے ترتیب دینے کا طریقہ چیک کریں"
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"languageName": "简体中文",
|
"languageName": "简体中文",
|
||||||
"Create your admin account": "创建你的管理员账号",
|
"Create your admin account": "创建你的管理员账号",
|
||||||
"authIncorrectCreds": "用户名或密码错误",
|
"authIncorrectCreds": "用户名或密码错误。",
|
||||||
"PasswordsDoNotMatch": "两次输入的密码不一致。",
|
"PasswordsDoNotMatch": "两次输入的密码不一致。",
|
||||||
"Repeat Password": "重复以确认密码",
|
"Repeat Password": "重复以确认密码",
|
||||||
"Create": "创建",
|
"Create": "创建",
|
||||||
"signedInDisp": "当前用户: {0}",
|
"signedInDisp": "当前用户: {0}",
|
||||||
"signedInDispDisabled": "已禁用身份验证",
|
"signedInDispDisabled": "已禁用身份验证。",
|
||||||
"home": "主页",
|
"home": "主页",
|
||||||
"console": "终端",
|
"console": "终端",
|
||||||
"registry": "镜像仓库",
|
"registry": "镜像仓库",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
"addFirstStackMsg": "组合你的第一个堆栈!",
|
"addFirstStackMsg": "组合你的第一个堆栈!",
|
||||||
"stackName" : "堆栈名称",
|
"stackName": "堆栈名称",
|
||||||
"deployStack": "部署",
|
"deployStack": "部署",
|
||||||
"deleteStack": "删除",
|
"deleteStack": "删除",
|
||||||
"stopStack": "停止",
|
"stopStack": "停止",
|
||||||
@ -22,9 +22,9 @@
|
|||||||
"editStack": "编辑",
|
"editStack": "编辑",
|
||||||
"discardStack": "放弃",
|
"discardStack": "放弃",
|
||||||
"saveStackDraft": "保存",
|
"saveStackDraft": "保存",
|
||||||
"notAvailableShort" : "不可用",
|
"notAvailableShort": "不可用",
|
||||||
"deleteStackMsg": "你确定要删除这个堆栈吗?",
|
"deleteStackMsg": "你确定要删除这个堆栈吗?",
|
||||||
"stackNotManagedByDockgeMsg": "这个堆栈不由Dockge管理",
|
"stackNotManagedByDockgeMsg": "这个堆栈不由Dockge管理。",
|
||||||
"primaryHostname": "主机名",
|
"primaryHostname": "主机名",
|
||||||
"general": "常规",
|
"general": "常规",
|
||||||
"container": "容器 | 容器组",
|
"container": "容器 | 容器组",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "允许使用的指令:",
|
"Allowed commands:": "允许使用的指令:",
|
||||||
"Internal Networks": "内部网络",
|
"Internal Networks": "内部网络",
|
||||||
"External Networks": "外部网络",
|
"External Networks": "外部网络",
|
||||||
"No External Networks": "无外部网络"
|
"No External Networks": "无外部网络",
|
||||||
|
"reconnecting...": "重连中…",
|
||||||
|
"reverseProxyMsg2": "检查如何配置WebSocket",
|
||||||
|
"reverseProxyMsg1": "正在使用反向代理?",
|
||||||
|
"connecting...": "正在连接到socket服务器…",
|
||||||
|
"Cannot connect to the socket server.": "无法连接到socket服务器。",
|
||||||
|
"url": "网址 | 网址",
|
||||||
|
"extra": "额外",
|
||||||
|
"downStack": "停止并删除"
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"languageName": "繁體中文(台灣)",
|
"languageName": "繁體中文(台灣)",
|
||||||
"Create your admin account": "建立您的管理員帳號",
|
"Create your admin account": "建立您的管理員帳號",
|
||||||
"authIncorrectCreds": "使用者名稱或密碼錯誤",
|
"authIncorrectCreds": "使用者名稱或密碼錯誤。",
|
||||||
"PasswordsDoNotMatch": "兩次輸入的密碼不一致。",
|
"PasswordsDoNotMatch": "兩次輸入的密碼不一致。",
|
||||||
"Repeat Password": "重複以確認密碼",
|
"Repeat Password": "重複以確認密碼",
|
||||||
"Create": "建立",
|
"Create": "建立",
|
||||||
"signedInDisp": "目前使用者:{0}",
|
"signedInDisp": "目前使用者:{0}",
|
||||||
"signedInDispDisabled": "已停用身份驗證",
|
"signedInDispDisabled": "已停用身份驗證。",
|
||||||
"home": "首頁",
|
"home": "首頁",
|
||||||
"console": "主控台",
|
"console": "主控台",
|
||||||
"registry": "映像倉庫",
|
"registry": "映像倉庫",
|
||||||
@ -24,7 +24,7 @@
|
|||||||
"saveStackDraft": "儲存",
|
"saveStackDraft": "儲存",
|
||||||
"notAvailableShort": "不可用",
|
"notAvailableShort": "不可用",
|
||||||
"deleteStackMsg": "您確定要刪除這個堆疊嗎?",
|
"deleteStackMsg": "您確定要刪除這個堆疊嗎?",
|
||||||
"stackNotManagedByDockgeMsg": "這個堆疊不由 Dockge 管理",
|
"stackNotManagedByDockgeMsg": "這個堆疊不由 Dockge 管理。",
|
||||||
"primaryHostname": "主機名稱",
|
"primaryHostname": "主機名稱",
|
||||||
"general": "一般",
|
"general": "一般",
|
||||||
"container": "容器 | 容器群組",
|
"container": "容器 | 容器群組",
|
||||||
@ -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": "使用者名稱",
|
||||||
@ -90,5 +90,13 @@
|
|||||||
"Allowed commands:": "允許使用的指令:",
|
"Allowed commands:": "允許使用的指令:",
|
||||||
"Internal Networks": "內部網路",
|
"Internal Networks": "內部網路",
|
||||||
"External Networks": "外部網路",
|
"External Networks": "外部網路",
|
||||||
"No External Networks": "無外部網路"
|
"No External Networks": "無外部網路",
|
||||||
|
"downStack": "停止",
|
||||||
|
"reverseProxyMsg1": "在使用反向代理嗎?",
|
||||||
|
"reverseProxyMsg2": "點擊這裡了解如何為 WebSocket 配置反向代理",
|
||||||
|
"Cannot connect to the socket server.": "無法連接到 Socket 伺服器。",
|
||||||
|
"reconnecting...": "重新連線中…",
|
||||||
|
"connecting...": "連線至 Socket 伺服器中…",
|
||||||
|
"url": "網址 | 網址",
|
||||||
|
"extra": "額外"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<div v-if="! $root.socketIO.connected && ! $root.socketIO.firstConnect" class="lost-connection">
|
<div v-if="! $root.socketIO.connected && ! $root.socketIO.firstConnect" class="lost-connection">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{{ $root.socketIO.connectionErrorMsg }}
|
{{ $root.socketIO.connectionErrorMsg }}
|
||||||
|
<div v-if="$root.socketIO.showReverseProxyGuide">
|
||||||
|
{{ $t("reverseProxyMsg1") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">{{ $t("reverseProxyMsg2") }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -13,8 +16,8 @@
|
|||||||
<span class="fs-4 title">Dockge</span>
|
<span class="fs-4 title">Dockge</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-info me-3">
|
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-warning me-3">
|
||||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
|
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("newUpdate") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
@ -82,6 +85,10 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
<div v-if="$root.socketIO.connecting" class="container mt-5">
|
||||||
|
<h4>{{ $t("connecting...") }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-view v-if="$root.loggedIn" />
|
<router-view v-if="$root.loggedIn" />
|
||||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||||
</main>
|
</main>
|
||||||
@ -91,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 {
|
||||||
|
|
||||||
@ -138,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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export default defineComponent({
|
|||||||
initedSocketIO: false,
|
initedSocketIO: false,
|
||||||
connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
|
connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
|
||||||
showReverseProxyGuide: true,
|
showReverseProxyGuide: true,
|
||||||
|
connecting: false,
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
|
|
||||||
@ -27,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 "🐬";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -64,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";
|
||||||
},
|
},
|
||||||
@ -83,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
|
||||||
@ -103,13 +158,24 @@ export default defineComponent({
|
|||||||
url = location.protocol + "//" + location.host;
|
url = location.protocol + "//" + location.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = io(url, {
|
let connectingMsgTimeout = setTimeout(() => {
|
||||||
transports: [ "websocket", "polling" ]
|
this.socketIO.connecting = true;
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
socket = io(url);
|
||||||
|
|
||||||
|
// 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", () => {
|
||||||
console.log("Connected to the socket server");
|
console.log("Connected to the socket server");
|
||||||
|
|
||||||
|
clearTimeout(connectingMsgTimeout);
|
||||||
|
this.socketIO.connecting = false;
|
||||||
|
|
||||||
this.socketIO.connectCount++;
|
this.socketIO.connectCount++;
|
||||||
this.socketIO.connected = true;
|
this.socketIO.connected = true;
|
||||||
this.socketIO.showReverseProxyGuide = false;
|
this.socketIO.showReverseProxyGuide = false;
|
||||||
@ -143,10 +209,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
socket.on("connect_error", (err) => {
|
socket.on("connect_error", (err) => {
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||||
this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`;
|
this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("reconnecting...")}`;
|
||||||
this.socketIO.showReverseProxyGuide = true;
|
this.socketIO.showReverseProxyGuide = true;
|
||||||
this.socketIO.connected = false;
|
this.socketIO.connected = false;
|
||||||
this.socketIO.firstConnect = false;
|
this.socketIO.firstConnect = false;
|
||||||
|
this.socketIO.connecting = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom Events
|
// Custom Events
|
||||||
@ -168,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);
|
||||||
@ -177,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,6 +269,25 @@ 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", () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -207,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
|
||||||
@ -297,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);
|
||||||
|
@ -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">
|
||||||
@ -41,7 +46,7 @@
|
|||||||
{{ $t("stopStack") }}
|
{{ $t("stopStack") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<BDropdown v-if="!isEditMode && active" right text="" variant="normal">
|
<BDropdown right text="" variant="normal">
|
||||||
<BDropdownItem @click="downStack">
|
<BDropdownItem @click="downStack">
|
||||||
<font-awesome-icon icon="stop" class="me-1" />
|
<font-awesome-icon icon="stop" class="me-1" />
|
||||||
{{ $t("downStack") }}
|
{{ $t("downStack") }}
|
||||||
@ -56,6 +61,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- URLs -->
|
||||||
|
<div v-if="urls.length > 0" class="mb-3">
|
||||||
|
<a v-for="(url, index) in urls" :key="index" target="_blank" :href="url.url">
|
||||||
|
<span class="badge bg-secondary me-2">{{ url.display }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress Terminal -->
|
<!-- Progress Terminal -->
|
||||||
<transition name="slide-fade" appear>
|
<transition name="slide-fade" appear>
|
||||||
<Terminal
|
<Terminal
|
||||||
@ -63,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>
|
||||||
@ -80,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>
|
||||||
|
|
||||||
@ -111,6 +134,20 @@
|
|||||||
|
|
||||||
<button v-if="false && isEditMode && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
<button v-if="false && isEditMode && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
||||||
|
|
||||||
|
<!-- General -->
|
||||||
|
<div v-if="isEditMode">
|
||||||
|
<h4 class="mb-3">{{ $t("extra") }}</h4>
|
||||||
|
<div class="shadow-box big-padding mb-3">
|
||||||
|
<!-- URLs -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
{{ $tc("url", 2) }}
|
||||||
|
</label>
|
||||||
|
<ArrayInput name="urls" :display-name="$t('url')" placeholder="https://" object-type="x-dockge" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Combined Terminal Output -->
|
<!-- Combined Terminal Output -->
|
||||||
<div v-show="!isEditMode">
|
<div v-show="!isEditMode">
|
||||||
<h4 class="mb-3">Terminal</h4>
|
<h4 class="mb-3">Terminal</h4>
|
||||||
@ -118,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;"
|
||||||
@ -133,7 +171,7 @@
|
|||||||
ref="editor"
|
ref="editor"
|
||||||
v-model="stack.composeYAML"
|
v-model="stack.composeYAML"
|
||||||
class="yaml-editor"
|
class="yaml-editor"
|
||||||
:highlight="highlighter"
|
:highlight="highlighterYAML"
|
||||||
line-numbers :readonly="!isEditMode"
|
line-numbers :readonly="!isEditMode"
|
||||||
@input="yamlCodeChange"
|
@input="yamlCodeChange"
|
||||||
@focus="editorFocus = true"
|
@focus="editorFocus = true"
|
||||||
@ -144,6 +182,22 @@
|
|||||||
{{ yamlError }}
|
{{ yamlError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ENV editor -->
|
||||||
|
<div v-if="isEditMode">
|
||||||
|
<h4 class="mb-3">.env</h4>
|
||||||
|
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
|
||||||
|
<prism-editor
|
||||||
|
ref="editor"
|
||||||
|
v-model="stack.composeENV"
|
||||||
|
class="env-editor"
|
||||||
|
:highlight="highlighterENV"
|
||||||
|
line-numbers :readonly="!isEditMode"
|
||||||
|
@focus="editorFocus = true"
|
||||||
|
@blur="editorFocus = false"
|
||||||
|
></prism-editor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditMode">
|
<div v-if="isEditMode">
|
||||||
<!-- Volumes -->
|
<!-- Volumes -->
|
||||||
<div v-if="false">
|
<div v-if="false">
|
||||||
@ -194,14 +248,15 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|||||||
import {
|
import {
|
||||||
COMBINED_TERMINAL_COLS,
|
COMBINED_TERMINAL_COLS,
|
||||||
COMBINED_TERMINAL_ROWS,
|
COMBINED_TERMINAL_ROWS,
|
||||||
copyYAMLComments,
|
copyYAMLComments, envsubstYAML,
|
||||||
getCombinedTerminalName,
|
getCombinedTerminalName,
|
||||||
getComposeTerminalName,
|
getComposeTerminalName,
|
||||||
PROGRESS_TERMINAL_ROWS,
|
PROGRESS_TERMINAL_ROWS,
|
||||||
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";
|
||||||
|
|
||||||
const template = `version: "3.8"
|
const template = `version: "3.8"
|
||||||
services:
|
services:
|
||||||
@ -211,10 +266,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
`;
|
`;
|
||||||
|
const envDefault = "# VARIABLE=value #comment";
|
||||||
|
|
||||||
let yamlErrorTimeout = null;
|
let yamlErrorTimeout = null;
|
||||||
|
|
||||||
let serviceStatusTimeout = null;
|
let serviceStatusTimeout = null;
|
||||||
|
let prismjsSymbolDefinition = {
|
||||||
|
"symbol": {
|
||||||
|
pattern: /(?<!\$)\$(\{[^{}]*\}|\w+)/,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -234,6 +295,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
editorFocus: false,
|
editorFocus: false,
|
||||||
jsonConfig: {},
|
jsonConfig: {},
|
||||||
|
envsubstJSONConfig: {},
|
||||||
yamlError: "",
|
yamlError: "",
|
||||||
processing: true,
|
processing: true,
|
||||||
showProgressTerminal: false,
|
showProgressTerminal: false,
|
||||||
@ -252,6 +314,38 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
endpointDisplay() {
|
||||||
|
return this.$root.endpointDisplayFunction(this.endpoint);
|
||||||
|
},
|
||||||
|
|
||||||
|
urls() {
|
||||||
|
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let urls = [];
|
||||||
|
for (const url of this.envsubstJSONConfig["x-dockge"].urls) {
|
||||||
|
let display;
|
||||||
|
try {
|
||||||
|
let obj = new URL(url);
|
||||||
|
let pathname = obj.pathname;
|
||||||
|
if (pathname === "/") {
|
||||||
|
pathname = "";
|
||||||
|
}
|
||||||
|
display = obj.host + pathname + obj.search;
|
||||||
|
} catch (e) {
|
||||||
|
display = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.push({
|
||||||
|
display,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
},
|
||||||
|
|
||||||
isAdd() {
|
isAdd() {
|
||||||
return this.$route.path === "/compose" && !this.submitted;
|
return this.$route.path === "/compose" && !this.submitted;
|
||||||
},
|
},
|
||||||
@ -261,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() {
|
||||||
@ -276,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": {
|
||||||
@ -301,6 +406,17 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"stack.composeENV": {
|
||||||
|
handler() {
|
||||||
|
if (this.editorFocus) {
|
||||||
|
console.debug("env code changed");
|
||||||
|
this.yamlCodeChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
|
||||||
jsonConfig: {
|
jsonConfig: {
|
||||||
handler() {
|
handler() {
|
||||||
if (!this.editorFocus) {
|
if (!this.editorFocus) {
|
||||||
@ -321,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() {
|
||||||
@ -332,20 +446,28 @@ export default {
|
|||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
|
|
||||||
let composeYAML;
|
let composeYAML;
|
||||||
|
let composeENV;
|
||||||
|
|
||||||
if (this.$root.composeTemplate) {
|
if (this.$root.composeTemplate) {
|
||||||
composeYAML = this.$root.composeTemplate;
|
composeYAML = this.$root.composeTemplate;
|
||||||
this.$root.composeTemplate = "";
|
this.$root.composeTemplate = "";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
composeYAML = template;
|
composeYAML = template;
|
||||||
}
|
}
|
||||||
|
if (this.$root.envTemplate) {
|
||||||
|
composeENV = this.$root.envTemplate;
|
||||||
|
this.$root.envTemplate = "";
|
||||||
|
} else {
|
||||||
|
composeENV = envDefault;
|
||||||
|
}
|
||||||
|
|
||||||
// Default Values
|
// Default Values
|
||||||
this.stack = {
|
this.stack = {
|
||||||
name: "",
|
name: "",
|
||||||
composeYAML,
|
composeYAML,
|
||||||
|
composeENV,
|
||||||
isManagedByDockge: true,
|
isManagedByDockge: true,
|
||||||
|
endpoint: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.yamlCodeChange();
|
this.yamlCodeChange();
|
||||||
@ -358,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 () => {
|
||||||
@ -371,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;
|
||||||
}
|
}
|
||||||
@ -384,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();
|
||||||
@ -441,15 +573,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bindTerminal(this.terminalName);
|
this.bindTerminal();
|
||||||
|
|
||||||
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -457,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.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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -471,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);
|
||||||
});
|
});
|
||||||
@ -480,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);
|
||||||
});
|
});
|
||||||
@ -489,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);
|
||||||
});
|
});
|
||||||
@ -498,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);
|
||||||
});
|
});
|
||||||
@ -507,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("/");
|
||||||
@ -527,32 +659,81 @@ export default {
|
|||||||
this.isEditMode = false;
|
this.isEditMode = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
highlighter(code) {
|
highlighterYAML(code) {
|
||||||
return highlight(code, languages.yaml);
|
if (!languages.yaml_with_symbols) {
|
||||||
|
languages.yaml_with_symbols = languages.insertBefore("yaml", "punctuation", {
|
||||||
|
"symbol": prismjsSymbolDefinition["symbol"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return highlight(code, languages.yaml_with_symbols);
|
||||||
|
},
|
||||||
|
|
||||||
|
highlighterENV(code) {
|
||||||
|
if (!languages.docker_env) {
|
||||||
|
languages.docker_env = {
|
||||||
|
"comment": {
|
||||||
|
pattern: /(^#| #).*$/m,
|
||||||
|
greedy: true
|
||||||
|
},
|
||||||
|
"keyword": {
|
||||||
|
pattern: /^\w*(?=[:=])/m,
|
||||||
|
greedy: true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
pattern: /(?<=[:=]).*?((?= #)|$)/m,
|
||||||
|
greedy: true,
|
||||||
|
inside: {
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
pattern: /^ *'.*?(?<!\\)'/m,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^ *".*?(?<!\\)"|^.*$/m,
|
||||||
|
inside: prismjsSymbolDefinition
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return highlight(code, languages.docker_env);
|
||||||
|
},
|
||||||
|
|
||||||
|
yamlToJSON(yaml) {
|
||||||
|
let doc = parseDocument(yaml);
|
||||||
|
if (doc.errors.length > 0) {
|
||||||
|
throw doc.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = doc.toJS() ?? {};
|
||||||
|
|
||||||
|
// Check data types
|
||||||
|
// "services" must be an object
|
||||||
|
if (!config.services) {
|
||||||
|
config.services = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
||||||
|
throw new Error("Services must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
doc,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
yamlCodeChange() {
|
yamlCodeChange() {
|
||||||
try {
|
try {
|
||||||
let doc = parseDocument(this.stack.composeYAML);
|
let { config, doc } = this.yamlToJSON(this.stack.composeYAML);
|
||||||
if (doc.errors.length > 0) {
|
|
||||||
throw doc.errors[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = doc.toJS() ?? {};
|
|
||||||
|
|
||||||
// Check data types
|
|
||||||
// "services" must be an object
|
|
||||||
if (!config.services) {
|
|
||||||
config.services = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(config.services) || typeof config.services !== "object") {
|
|
||||||
throw new Error("Services must be an object");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.yamlDoc = doc;
|
this.yamlDoc = doc;
|
||||||
this.jsonConfig = config;
|
this.jsonConfig = config;
|
||||||
|
|
||||||
|
let env = dotenv.parse(this.stack.composeENV);
|
||||||
|
let envYAML = envsubstYAML(this.stack.composeYAML, env);
|
||||||
|
this.envsubstJSONConfig = this.yamlToJSON(envYAML).config;
|
||||||
|
|
||||||
clearTimeout(yamlErrorTimeout);
|
clearTimeout(yamlErrorTimeout);
|
||||||
this.yamlError = "";
|
this.yamlError = "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -610,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;
|
||||||
}
|
}
|
||||||
@ -621,4 +804,9 @@ export default {
|
|||||||
background-color: #2c2f38 !important;
|
background-color: #2c2f38 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $dark-font-color3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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() {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
38
package.json
38
package.json
@ -1,25 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "dockge",
|
"name": "dockge",
|
||||||
"version": "1.1.1",
|
"version": "1.3.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0 && <= 18.17.1"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fmt": "eslint \"**/*.{ts,vue}\" --fix",
|
"fmt": "eslint \"**/*.{ts,vue}\" --fix",
|
||||||
"lint": "eslint \"**/*.{ts,vue}\"",
|
"lint": "eslint \"**/*.{ts,vue}\"",
|
||||||
"check-ts": "tsc --noEmit",
|
"check-ts": "tsc --noEmit",
|
||||||
"start": "tsx ./backend/index.ts",
|
"start": "tsx ./backend/index.ts",
|
||||||
|
"dev": "concurrently -k -r \"wait-on tcp:5000 && pnpm run dev:backend \" \"pnpm run dev:frontend\"",
|
||||||
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts",
|
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts",
|
||||||
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
|
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
|
||||||
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
|
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
|
||||||
|
"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 --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",
|
||||||
"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",
|
||||||
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts",
|
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts",
|
||||||
"reformat-changelog": "tsx ./extra/reformat-changelog.ts"
|
"reformat-changelog": "tsx ./extra/reformat-changelog.ts",
|
||||||
|
"reset-password": "tsx ./extra/reset-password.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.11",
|
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.12",
|
||||||
|
"@inventage/envsubst": "^0.16.0",
|
||||||
"@louislam/sqlite3": "~15.1.6",
|
"@louislam/sqlite3": "~15.1.6",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"check-password-strength": "~2.0.7",
|
"check-password-strength": "~2.0.7",
|
||||||
@ -28,6 +37,7 @@
|
|||||||
"composerize": "~1.4.1",
|
"composerize": "~1.4.1",
|
||||||
"croner": "~7.0.5",
|
"croner": "~7.0.5",
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.10",
|
||||||
|
"dotenv": "~16.3.1",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"http-graceful-shutdown": "~3.1.13",
|
"http-graceful-shutdown": "~3.1.13",
|
||||||
@ -35,33 +45,39 @@
|
|||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
"knex": "~2.5.1",
|
"knex": "~2.5.1",
|
||||||
"limiter-es6-compat": "~2.1.2",
|
"limiter-es6-compat": "~2.1.2",
|
||||||
"mysql2": "~3.6.3",
|
"mysql2": "~3.6.5",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"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",
|
||||||
"ts-command-line-args": "~2.5.1",
|
"ts-command-line-args": "~2.5.1",
|
||||||
"tsx": "~3.14.0",
|
"tsx": "~4.6.2",
|
||||||
"type-fest": "~4.3.3",
|
"type-fest": "~4.3.3",
|
||||||
"yaml": "~2.3.4"
|
"yaml": "~2.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.0",
|
||||||
"@fontsource/jetbrains-mono": "^5.0.17",
|
"@fontsource/jetbrains-mono": "^5.0.18",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/bootstrap": "~5.2.9",
|
"@types/bootstrap": "~5.2.10",
|
||||||
"@types/command-exists": "~1.2.3",
|
"@types/command-exists": "~1.2.3",
|
||||||
"@types/express": "~4.17.21",
|
"@types/express": "~4.17.21",
|
||||||
"@types/jsonwebtoken": "~9.0.5",
|
"@types/jsonwebtoken": "~9.0.5",
|
||||||
|
"@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.0",
|
"@vitejs/plugin-vue": "~4.5.2",
|
||||||
|
"@xterm/addon-fit": "beta",
|
||||||
|
"@xterm/xterm": "beta",
|
||||||
"bootstrap": "5.3.2",
|
"bootstrap": "5.3.2",
|
||||||
"bootstrap-vue-next": "~0.14.10",
|
"bootstrap-vue-next": "~0.14.10",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"eslint": "~8.50.0",
|
"eslint": "~8.50.0",
|
||||||
"eslint-plugin-jsdoc": "~46.8.2",
|
"eslint-plugin-jsdoc": "~46.8.2",
|
||||||
@ -70,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.0",
|
"vite": "~5.0.10",
|
||||||
"vite-plugin-compression": "~0.5.1",
|
"vite-plugin-compression": "~0.5.1",
|
||||||
"vue": "~3.3.8",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1456
pnpm-lock.yaml
generated
1456
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
|||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"backend/**/*"
|
"backend/**/*",
|
||||||
|
"common/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user