Compare commits

..

27 Commits

Author SHA1 Message Date
d8d25563ba wip 2023-11-11 22:10:22 +08:00
83d58cd363 Fix composerize 2023-11-11 21:29:09 +08:00
caa82bbad5 wip 2023-11-11 19:14:27 +08:00
2530cac989 wip 2023-11-11 11:17:26 +08:00
06307956ca wip 2023-11-10 22:06:58 +08:00
9b9234434e wip 2023-11-10 21:52:38 +08:00
a12c6dc033 wip 2023-11-10 18:16:52 +08:00
eb6db8b31e wip 2023-11-07 21:23:05 +08:00
015e4c21f9 wip 2023-11-07 21:23:05 +08:00
7a63d59ef8 Update README.md 2023-11-07 00:13:07 +08:00
ecbbdae7ab wip 2023-11-06 23:55:22 +08:00
e7abbcbefa Update README.md 2023-11-06 23:52:43 +08:00
59adcc148c wip 2023-11-06 23:51:08 +08:00
5e9065a4d6 wip 2023-11-06 23:25:54 +08:00
35a79ea8a6 wip 2023-11-06 23:21:18 +08:00
0c9fc4ead2 wip 2023-11-06 23:18:11 +08:00
1c5ff7914b wip 2023-11-06 23:16:08 +08:00
f7c66a476c wip 2023-11-06 23:14:02 +08:00
5daa6fd788 wip 2023-11-06 23:07:52 +08:00
c2ec9ac7f4 wip 2023-11-06 22:54:20 +08:00
21e736459e wip 2023-11-06 21:24:06 +08:00
d7f4873405 wip 2023-11-06 16:00:20 +08:00
2ed739b1b9 wip 2023-11-06 15:15:55 +08:00
314630724b wip 2023-11-06 01:18:02 +08:00
e67d08b7b3 wip 2023-10-29 15:25:52 +08:00
7d1da2ad99 wip 2023-10-26 13:23:45 +08:00
5f70fa6baf wip 2023-10-23 19:30:58 +08:00
52 changed files with 363 additions and 2030 deletions

View File

@ -92,9 +92,6 @@ module.exports = {
"one-var": [ "error", "never" ], "one-var": [ "error", "never" ],
"max-statements-per-line": [ "error", { "max": 1 }], "max-statements-per-line": [ "error", { "max": 1 }],
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [ "warn", {
"args": "none"
}],
"prefer-const" : "off", "prefer-const" : "off",
}, },
}; };

View File

@ -1,60 +0,0 @@
name: Node.js CI - Dockge
on:
push:
branches: [master]
paths-ignore:
- '*.md'
pull_request:
branches: [master]
paths-ignore:
- '*.md'
jobs:
ci:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20.x] # Can be changed
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm run lint
- name: Check Typescript
run: pnpm run check-ts
# more things can be add later like tests etc..

View File

@ -4,48 +4,34 @@
# Dockge # Dockge
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager. A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager.
![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github) ![GitHub issues](https://img.shields.io/github/issues/louislam/dockge?logo=github) ![GitHub pull requests](https://img.shields.io/github/issues-pr/louislam/dockge?logo=github) ![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge?logo=docker) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github) ![GitHub](https://img.shields.io/github/license/louislam/dockge?logo=github)
<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="" />
View Video: https://youtu.be/AWAlOQeNpgU?t=48
## ⭐ Features ## ⭐ Features
- Manage `compose.yaml` - Manage `compose.yaml`
- Create/Edit/Start/Stop/Restart/Delete
- Update Docker Images
- Interactive Editor for `compose.yaml` - Interactive Editor for `compose.yaml`
- Interactive Web Terminal - Interactive Web Terminal
- Reactive - Reactive
- Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time
- Easy-to-use & fancy UI - Easy-to-use & fancy UI
- If you love Uptime Kuma's UI/UX, you will love this one too - If you love Uptime Kuma's UI/UX, you will love this too
- Convert `docker run ...` commands into `compose.yaml` - 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 />
![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a)
## 🔧 How to Install ## 🔧 How to Install
Requirements: Requirements:
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman - [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended
- (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/) - [Docker Compose V2](https://docs.docker.com/compose/install/linux/)
- (Podman only) podman-docker (Debian: `apt install podman-docker`) - OS:
- OS: - As long as you can run Docker CE, it should be fine, but:
- As long as you can run Docker CE / Podman, it should be fine, but: - Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher
- Arch: armv7, arm64, amd64 (a.k.a x86_64) - Arch: armv7, arm64, amd64 (a.k.a x86_64)
### Basic ### Basic
- Default Stacks Directory: `/opt/stacks` Default stacks directory is `/opt/stacks`.
- Default Port: 5001
``` ```
# Create a directory that stores your stacks and stores dockge's compose.yaml # Create a directory that stores your stacks and stores dockge's compose.yaml
@ -55,19 +41,19 @@ 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 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
# docker-compose up -d # docker-compose up -d
``` ```
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 change the `DOCKGE_STACKS_DIR` environment variable and volumes.
For exmaples, if you want to store your stacks in `/my-stacks`:
```yaml ```yaml
version: "3.8" version: "3.8"
services: services:
@ -75,76 +61,48 @@ services:
image: louislam/dockge:1 image: louislam/dockge:1
restart: unless-stopped restart: unless-stopped
ports: ports:
# Host Port:Container Port
- 5001:5001 - 5001:5001
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data - ./data:/app/data
# If you want to use private registries, you need to share the auth file with Dockge: # Your stacks directory in the host
# - /root/.docker/:/root/.docker # (The paths inside container must be the same as the host)
- /my-stacks:/my-stacks
# 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 EXAMPLE: - /my-stacks:/my-stacks (Both paths match)
# ❌❌❌❌ WRONG EXAMPLE: - /docker:/my-stacks (Both paths do not match)
- /opt/stacks:/opt/stacks
environment: environment:
# Tell Dockge where is your stacks directory # Tell Dockge where is your stacks directory
- DOCKGE_STACKS_DIR=/opt/stacks - DOCKGE_STACKS_DIR=/my-stacks
``` ```
## How to Update ## How to Update
```bash ```bash
cd /opt/dockge cd /opt/stacks
docker compose pull docker compose pull
docker compose up -d docker compose up -d
``` ```
## Screenshots
![](https://github.com/louislam/dockge/assets/1336778/e7ff0222-af2e-405c-b533-4eab04791b40)
![](https://github.com/louislam/dockge/assets/1336778/7139e88c-77ed-4d45-96e3-00b66d36d871)
![](https://github.com/louislam/dockge/assets/1336778/f019944c-0e87-405b-a1b8-625b35de1eeb)
![](https://github.com/louislam/dockge/assets/1336778/a4478d23-b1c4-4991-8768-1a7cad3472e3)
## Motivations ## Motivations
- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. - I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear.
- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they don't have support for arm64, so I stepped back to Node.js) - Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js)
If you love this project, please consider giving it a ⭐. If you love this project, please consider giving this project a ⭐.
## 🗣️
### Bug Report
https://github.com/louislam/dockge/issues
### Ask for Help / Discussions
https://github.com/louislam/dockge/discussions
## 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)
## 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 hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`.
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`. The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. They are all ending with `-ge`.
If you are not comfortable with the pronunciation, you can call it `Dockage`
#### Can I manage a single container without `compose.yaml`? #### Can I manage a single container without `compose.yaml`?
The main objective of Dockge is to try to use the docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI. The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI.
#### Can I manage existing stacks? #### Can I manage existing stacks?
@ -168,4 +126,6 @@ Yes, you can. However, you need to move your compose file into the stacks direct
# Others # 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` is also known as `docker-compose.yml`.

View File

@ -5,7 +5,6 @@ import fs from "fs";
import path from "path"; import path from "path";
import knex from "knex"; import knex from "knex";
// @ts-ignore
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";
@ -13,11 +12,6 @@ import { sleep } from "./util-common";
interface DBConfig { interface DBConfig {
type?: "sqlite" | "mysql"; type?: "sqlite" | "mysql";
hostname?: string;
port?: string;
database?: string;
username?: string;
password?: string;
} }
export class Database { export class Database {
@ -25,7 +19,7 @@ export class Database {
* SQLite file path (Default: ./data/dockge.db) * SQLite file path (Default: ./data/dockge.db)
* @type {string} * @type {string}
*/ */
static sqlitePath : string; static sqlitePath;
static noReject = true; static noReject = true;
@ -57,7 +51,7 @@ export class Database {
* @typedef {string|undefined} envString * @typedef {string|undefined} envString
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
*/ */
static readDBConfig() : DBConfig { static readDBConfig() {
const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8"); const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8");
const dbConfig = JSON.parse(dbConfigString); const dbConfig = JSON.parse(dbConfigString);
@ -73,10 +67,10 @@ export class Database {
/** /**
* @typedef {string|undefined} envString * @typedef {string|undefined} envString
* @param dbConfig the database configuration that should be written * @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
* @returns {void} * @returns {void}
*/ */
static writeDBConfig(dbConfig : DBConfig) { static writeDBConfig(dbConfig) {
fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4)); fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
} }
@ -86,17 +80,14 @@ export class Database {
* @param {boolean} noLog Should logs not be output? * @param {boolean} noLog Should logs not be output?
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async connect(autoloadModels = true) { static async connect(autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000; const acquireConnectionTimeout = 120 * 1000;
let dbConfig : DBConfig; let dbConfig;
try { try {
dbConfig = this.readDBConfig(); dbConfig = this.readDBConfig();
Database.dbConfig = dbConfig; Database.dbConfig = dbConfig;
} catch (err) { } catch (err) {
if (err instanceof Error) { log.warn("db", err.message);
log.warn("db", err.message);
}
dbConfig = { dbConfig = {
type: "sqlite", type: "sqlite",
}; };
@ -185,15 +176,13 @@ export class Database {
directory: Database.knexMigrationsPath, directory: Database.knexMigrationsPath,
}); });
} catch (e) { } catch (e) {
if (e instanceof Error) { // Allow missing patch files for downgrade or testing pr.
// Allow missing patch files for downgrade or testing pr. if (e.message.includes("the following files are missing:")) {
if (e.message.includes("the following files are missing:")) { log.warn("db", e.message);
log.warn("db", e.message); log.warn("db", "Database migration failed, you may be downgrading Dockge.");
log.warn("db", "Database migration failed, you may be downgrading Dockge."); } else {
} else { log.error("db", "Database migration failed");
log.error("db", "Database migration failed"); throw e;
throw e;
}
} }
} }
} }

View File

@ -60,7 +60,7 @@ export class DockgeServer {
*/ */
needSetup = false; needSetup = false;
jwtSecret : string = ""; jwtSecret? : string;
stacksDir : string = ""; stacksDir : string = "";
@ -129,7 +129,7 @@ export class DockgeServer {
this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined; this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined; this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined; this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
this.config.port = args.port || Number(process.env.DOCKGE_PORT) || 5001; this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
@ -218,7 +218,7 @@ 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(socket as DockgeSocket, await R.findOne("user"));
socket.emit("autoLogin"); socket.emit("autoLogin");
} else { } else {
log.debug("auth", "need auth"); log.debug("auth", "need auth");
@ -253,9 +253,7 @@ export class DockgeServer {
try { try {
await Database.init(this); await Database.init(this);
} catch (e) { } catch (e) {
if (e instanceof Error) { log.error("server", "Failed to prepare your database: " + e.message);
log.error("server", "Failed to prepare your database: " + e.message);
}
process.exit(1); process.exit(1);
} }
@ -293,7 +291,7 @@ export class DockgeServer {
} }
// Run every 5 seconds // Run every 5 seconds
Cron("*/2 * * * * *", { const job = Cron("*/2 * * * * *", {
protect: true, // Enabled over-run protection. protect: true, // Enabled over-run protection.
}, () => { }, () => {
log.debug("server", "Cron job running"); log.debug("server", "Cron job running");
@ -378,9 +376,7 @@ export class DockgeServer {
return process.env.TZ; return process.env.TZ;
} }
} catch (e) { } catch (e) {
if (e instanceof Error) { log.warn("timezone", e.message + " in process.env.TZ");
log.warn("timezone", e.message + " in process.env.TZ");
}
} }
const timezone = await Settings.get("serverTimezone"); const timezone = await Settings.get("serverTimezone");
@ -393,9 +389,7 @@ export class DockgeServer {
return timezone; return timezone;
} }
} catch (e) { } catch (e) {
if (e instanceof Error) { log.warn("timezone", e.message + " in settings");
log.warn("timezone", e.message + " in settings");
}
} }
// Guess // Guess

View File

@ -103,10 +103,6 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized. * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/ */
log(module: string, msg: unknown, level: string) { log(module: string, msg: unknown, level: string) {
if (level === "DEBUG" && !isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) { if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return; return;
} }

View File

@ -17,7 +17,7 @@ export function generatePasswordHash(password : string) {
* @param {string} hash Hash to verify against * @param {string} hash Hash to verify against
* @returns {boolean} Does the password match the hash? * @returns {boolean} Does the password match the hash?
*/ */
export function verifyPassword(password : string, hash : string) { export function verifyPassword(password, hash) {
return bcrypt.compareSync(password, hash); return bcrypt.compareSync(password, hash);
} }
@ -37,7 +37,7 @@ export const SHAKE256_LENGTH = 16;
* @param {number} len Output length of the hash * @param {number} len Output length of the hash
* @returns {string} The hashed data in hex format * @returns {string} The hashed data in hex format
*/ */
export function shake256(data : string, len : number) { export function shake256(data, len) {
if (!data) { if (!data) {
return ""; return "";
} }

View File

@ -1,14 +1,8 @@
// "limit" is bugged in Typescript, use "limiter-es6-compat" instead // "limit" is bugged in Typescript, use "limiter-es6-compat" instead
// See https://github.com/jhurliman/node-rate-limiter/issues/80 // See https://github.com/jhurliman/node-rate-limiter/issues/80
import { RateLimiter, RateLimiterOpts } from "limiter-es6-compat"; import { RateLimiter } from "limiter-es6-compat";
import { log } from "./log"; import { log } from "./log";
export interface KumaRateLimiterOpts extends RateLimiterOpts {
errorMessage : string;
}
export type KumaRateLimiterCallback = (err : object) => void;
class KumaRateLimiter { class KumaRateLimiter {
errorMessage : string; errorMessage : string;
@ -17,7 +11,7 @@ class KumaRateLimiter {
/** /**
* @param {object} config Rate limiter configuration object * @param {object} config Rate limiter configuration object
*/ */
constructor(config : KumaRateLimiterOpts) { constructor(config) {
this.errorMessage = config.errorMessage; this.errorMessage = config.errorMessage;
this.rateLimiter = new RateLimiter(config); this.rateLimiter = new RateLimiter(config);
} }
@ -30,11 +24,11 @@ class KumaRateLimiter {
/** /**
* Should the request be passed through * Should the request be passed through
* @param callback Callback function to call with decision * @param {passCB} callback Callback function to call with decision
* @param {number} num Number of tokens to remove * @param {number} num Number of tokens to remove
* @returns {Promise<boolean>} Should the request be allowed? * @returns {Promise<boolean>} Should the request be allowed?
*/ */
async pass(callback : KumaRateLimiterCallback, num = 1) { async pass(callback, num = 1) {
const remainingRequests = await this.removeTokens(num); const remainingRequests = await this.removeTokens(num);
log.info("rate-limit", "remaining requests: " + remainingRequests); log.info("rate-limit", "remaining requests: " + remainingRequests);
if (remainingRequests < 0) { if (remainingRequests < 0) {

View File

@ -1,4 +1,4 @@
import { DockgeServer } from "../dockge-server"; import { DockgeServer } from "../dockgeServer";
import { Router } from "../router"; import { Router } from "../router";
import express, { Express, Router as ExpressRouter } from "express"; import express, { Express, Router as ExpressRouter } from "express";

View File

@ -1,6 +1,5 @@
import { R } from "redbean-node"; import { R } from "redbean-node";
import { log } from "./log"; import { log } from "./log";
import { LooseObject } from "./util-common";
export class Settings { export class Settings {
@ -16,19 +15,20 @@ export class Settings {
* timestamp: 12345678 * timestamp: 12345678
* }, * },
* } * }
* @type {{}}
*/ */
static cacheList : LooseObject = { static cacheList = {
}; };
static cacheCleaner? : NodeJS.Timeout; static cacheCleaner = null;
/** /**
* Retrieve value of setting based on key * Retrieve value of setting based on key
* @param key Key of setting to retrieve * @param {string} key Key of setting to retrieve
* @returns Value * @returns {Promise<any>} Value
*/ */
static async get(key : string) { static async get(key) {
// Start cache clear if not started yet // Start cache clear if not started yet
if (!Settings.cacheCleaner) { if (!Settings.cacheCleaner) {
@ -72,12 +72,12 @@ export class Settings {
/** /**
* Sets the specified setting to specified value * Sets the specified setting to specified value
* @param key Key of setting to set * @param {string} key Key of setting to set
* @param value Value to set to * @param {any} value Value to set to
* @param {?string} type Type of setting * @param {?string} type Type of setting
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async set(key : string, value : object | string | number | boolean, type : string | null = null) { static async set(key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
@ -95,15 +95,15 @@ export class Settings {
/** /**
* Get settings based on type * Get settings based on type
* @param type The type of setting * @param {string} type The type of setting
* @returns Settings * @returns {Promise<Bean>} Settings
*/ */
static async getSettings(type : string) { static async getSettings(type) {
const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type, type,
]); ]);
const result : LooseObject = {}; const result = {};
for (const row of list) { for (const row of list) {
try { try {
@ -118,11 +118,11 @@ export class Settings {
/** /**
* Set settings based on type * Set settings based on type
* @param type Type of settings to set * @param {string} type Type of settings to set
* @param data Values of settings * @param {object} data Values of settings
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async setSettings(type : string, data : LooseObject) { static async setSettings(type, data) {
const keyList = Object.keys(data); const keyList = Object.keys(data);
const promiseList = []; const promiseList = [];
@ -154,7 +154,7 @@ export class Settings {
* @param {string[]} keyList Keys to remove * @param {string[]} keyList Keys to remove
* @returns {void} * @returns {void}
*/ */
static deleteCache(keyList : string[]) { static deleteCache(keyList) {
for (const key of keyList) { for (const key of keyList) {
delete Settings.cacheList[key]; delete Settings.cacheList[key];
} }
@ -167,7 +167,7 @@ export class Settings {
static stopCacheCleaner() { static stopCacheCleaner() {
if (Settings.cacheCleaner) { if (Settings.cacheCleaner) {
clearInterval(Settings.cacheCleaner); clearInterval(Settings.cacheCleaner);
Settings.cacheCleaner = undefined; Settings.cacheCleaner = null;
} }
} }
} }

View File

@ -196,7 +196,7 @@ 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 = Stack.getStack(server, stackName);
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({ callback({
ok: true, ok: true,

View File

@ -1,11 +1,12 @@
import { SocketHandler } from "../socket-handler.js"; import { SocketHandler } from "../socket-handler.js";
import { Socket } from "socket.io";
import { DockgeServer } from "../dockge-server"; import { DockgeServer } from "../dockge-server";
import { log } from "../log"; import { log } from "../log";
import { R } from "redbean-node"; 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 { checkLogin, DockgeSocket, doubleCheckPassword } 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";
@ -42,12 +43,10 @@ export class MainSocketHandler extends SocketHandler {
}); });
} catch (e) { } catch (e) {
if (e instanceof Error) { callback({
callback({ ok: false,
ok: false, msg: e.message,
msg: e.message, });
});
}
} }
}); });
@ -58,7 +57,7 @@ export class MainSocketHandler extends SocketHandler {
log.info("auth", `Login by token. IP=${clientIP}`); log.info("auth", `Login by token. IP=${clientIP}`);
try { try {
const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded; const decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username); log.info("auth", "Username from JWT: " + decoded.username);
@ -92,13 +91,9 @@ export class MainSocketHandler extends SocketHandler {
}); });
} }
} catch (error) { } catch (error) {
if (!(error instanceof Error)) {
console.error("Unknown error:", error);
return;
}
log.error("auth", `Invalid token. IP=${clientIP}`); log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) { if (error.message) {
log.error("auth", error.message + ` IP=${clientIP}`); log.error("auth", error.message, `IP=${clientIP}`);
} }
callback({ callback({
ok: false, ok: false,
@ -154,7 +149,6 @@ export class MainSocketHandler extends SocketHandler {
} }
if (data.token) { if (data.token) {
// @ts-ignore
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== data.token && verify) { if (user.twofa_last_token !== data.token && verify) {
@ -217,12 +211,10 @@ export class MainSocketHandler extends SocketHandler {
}); });
} catch (e) { } catch (e) {
if (e instanceof Error) { callback({
callback({ ok: false,
ok: false, msg: e.message,
msg: e.message, });
});
}
} }
}); });
@ -237,12 +229,10 @@ export class MainSocketHandler extends SocketHandler {
}); });
} catch (e) { } catch (e) {
if (e instanceof Error) { callback({
callback({ ok: false,
ok: false, msg: e.message,
msg: e.message, });
});
}
} }
}); });
@ -272,24 +262,22 @@ export class MainSocketHandler extends SocketHandler {
server.sendInfo(socket); server.sendInfo(socket);
} catch (e) { } catch (e) {
if (e instanceof Error) { callback({
callback({ ok: false,
ok: false, msg: e.message,
msg: e.message, });
});
}
} }
}); });
} }
async login(username : string, password : string) : Promise<User | null> { async login(username : string, password : string) {
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
return null; return null;
} }
const user = await R.findOne("user", " username = ? AND active = 1 ", [ const user = await R.findOne("user", " username = ? AND active = 1 ", [
username, username,
]) as User; ]);
if (user && verifyPassword(password, user.password)) { if (user && verifyPassword(password, user.password)) {
// Upgrade the hash to bcrypt // Upgrade the hash to bcrypt

View File

@ -38,12 +38,10 @@ 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) { errorCallback({
errorCallback({ ok: false,
ok: false, msg: e.message,
msg: e.message, });
});
}
} }
}); });

View File

@ -24,28 +24,16 @@ export class Stack {
protected _status: number = UNKNOWN; protected _status: number = UNKNOWN;
protected _composeYAML?: string; protected _composeYAML?: string;
protected _configFilePath?: string; protected _configFilePath?: string;
protected _composeFileName: string = "compose.yaml";
protected server: DockgeServer; protected server: DockgeServer;
protected combinedTerminal? : Terminal; protected combinedTerminal? : Terminal;
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) {
this.name = name; this.name = name;
this.server = server; this.server = server;
this._composeYAML = composeYAML; this._composeYAML = composeYAML;
if (!skipFSOperations) {
// Check if compose file name is different from compose.yaml
const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
for (const filename of supportedFileNames) {
if (fs.existsSync(path.join(this.path, filename))) {
this._composeFileName = filename;
break;
}
}
}
} }
toJSON() : object { toJSON() : object {
@ -62,7 +50,6 @@ export class Stack {
status: this._status, status: this._status,
tags: [], tags: [],
isManagedByDockge: this.isManagedByDockge, isManagedByDockge: this.isManagedByDockge,
composeFileName: this._composeFileName,
}; };
} }
@ -85,9 +72,9 @@ export class Stack {
} }
validate() { validate() {
// Check name, allows [a-z][0-9] _ - only // Check name, allows [a-z][A-Z][0-9] _ - only
if (!this.name.match(/^[a-z0-9_-]+$/)) { if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only"); throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
} }
// Check YAML format // Check YAML format
@ -97,7 +84,7 @@ export class Stack {
get composeYAML() : string { get composeYAML() : string {
if (this._composeYAML === undefined) { if (this._composeYAML === undefined) {
try { try {
this._composeYAML = fs.readFileSync(path.join(this.path, this._composeFileName), "utf-8"); this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
} catch (e) { } catch (e) {
this._composeYAML = ""; this._composeYAML = "";
} }
@ -148,7 +135,7 @@ export class Stack {
} }
// Write or overwrite the compose.yaml // Write or overwrite the compose.yaml
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML); fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
} }
async deploy(socket? : DockgeSocket) : Promise<number> { async deploy(socket? : DockgeSocket) : Promise<number> {
@ -162,7 +149,7 @@ export class Stack {
async delete(socket?: DockgeSocket) : Promise<number> { async delete(socket?: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); const terminalName = getComposeTerminalName(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", "--rmi", "all" ], 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.");
} }
@ -176,17 +163,6 @@ export class Stack {
return exitCode; return exitCode;
} }
updateStatus() {
let statusList = Stack.getStatusList();
let status = statusList.get(this.name);
if (status) {
this._status = status;
} else {
this._status = UNKNOWN;
}
}
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> { static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
let stacksDir = server.stacksDir; let stacksDir = server.stacksDir;
let stackList : Map<string, Stack>; let stackList : Map<string, Stack>;
@ -201,18 +177,11 @@ export class Stack {
for (let filename of filenameList) { for (let filename of filenameList) {
try { try {
// Check if it is a directory
let stat = fs.statSync(path.join(stacksDir, filename));
if (!stat.isDirectory()) {
continue;
}
let stack = this.getStack(server, filename); let stack = this.getStack(server, filename);
stack._status = CREATED_FILE; stack._status = CREATED_FILE;
stackList.set(filename, stack); stackList.set(filename, stack);
} catch (e) { } catch (e) {
if (e instanceof Error) { log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
} }
} }
@ -266,51 +235,37 @@ export class Stack {
/** /**
* Convert the status string from `docker compose ls` to the status number * Convert the status string from `docker compose ls` to the status number
* Input Example: "exited(1), running(1)"
* @param status * @param status
*/ */
static statusConvert(status : string) : number { static statusConvert(status : string) : number {
if (status.startsWith("created")) { if (status.startsWith("created")) {
return CREATED_STACK; return CREATED_STACK;
} else if (status.includes("exited")) {
// If one of the service is exited, we consider the stack is exited
return EXITED;
} else if (status.startsWith("running")) { } else if (status.startsWith("running")) {
// If there is no exited services, there should be only running services
return RUNNING; return RUNNING;
} else if (status.startsWith("exited")) {
return EXITED;
} else { } else {
return UNKNOWN; return UNKNOWN;
} }
} }
static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack { static getStack(server: DockgeServer, stackName: string) : Stack {
let dir = path.join(server.stacksDir, stackName); let dir = path.join(server.stacksDir, stackName);
if (!skipFSOperations) { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
if (!fs.existsSync(dir) || !fs.statSync(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);
let stackList = this.getStackList(server, true); let stack = stackList.get(stackName);
let stack = stackList.get(stackName);
if (stack) { if (stack) {
return stack; return stack;
} else { } else {
// Really not found // Really not found
throw new ValidationError("Stack not found"); throw new ValidationError("Stack not found");
}
} }
} else {
log.debug("getStack", "Skip FS operations");
}
let stack : Stack;
if (!skipFSOperations) {
stack = new Stack(server, stackName);
} else {
stack = new Stack(server, stackName, undefined, true);
} }
let stack = new Stack(server, stackName);
stack._status = UNKNOWN; stack._status = UNKNOWN;
stack._configFilePath = path.resolve(dir); stack._configFilePath = path.resolve(dir);
return stack; return stack;
@ -349,14 +304,6 @@ export class Stack {
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
this.updateStatus();
log.debug("update", "Status: " + this.status);
if (this.status !== RUNNING) {
return exitCode;
}
exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); 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 restart, please check the terminal output for more information."); throw new Error("Failed to restart, please check the terminal output for more information.");
@ -390,20 +337,16 @@ 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" ], { let res = childProcess.execSync("docker compose ps --format json", {
cwd: this.path, cwd: this.path,
}); });
let lines = res.stdout.toString().split("\n"); let lines = res.toString().split("\n");
for (let line of lines) { for (let line of lines) {
try { try {
let obj = JSON.parse(line); let obj = JSON.parse(line);
if (obj.Health === "") { statusList.set(obj.Service, obj.State);
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) { } catch (e) {
} }
} }

View File

@ -54,9 +54,7 @@ export class Terminal {
try { try {
this.ptyProcess?.resize(this.cols, this.rows); this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) { } catch (e) {
if (e instanceof Error) { log.debug("Terminal", "Failed to resize terminal: " + e.message);
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
} }
} }
@ -69,9 +67,7 @@ export class Terminal {
try { try {
this.ptyProcess?.resize(this.cols, this.rows); this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) { } catch (e) {
if (e instanceof Error) { log.debug("Terminal", "Failed to resize terminal: " + e.message);
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
} }
} }
@ -80,53 +76,37 @@ export class Terminal {
return; return;
} }
try { this._ptyProcess = pty.spawn(this.file, this.args, {
this._ptyProcess = pty.spawn(this.file, this.args, { name: this.name,
name: this.name, cwd: this.cwd,
cwd: this.cwd, cols: TERMINAL_COLS,
cols: TERMINAL_COLS, rows: this.rows,
rows: this.rows, });
});
// On Data // On Data
this._ptyProcess.onData((data) => { this._ptyProcess.onData((data) => {
this.buffer.pushItem(data); this.buffer.push(data);
if (this.server.io) { if (this.server.io) {
this.server.io.to(this.name).emit("terminalWrite", this.name, data); this.server.io.to(this.name).emit("terminalWrite", this.name, data);
}
});
// On Exit
this._ptyProcess.onExit(this.exit);
} catch (error) {
if (error instanceof Error) {
log.error("Terminal", "Failed to start terminal: " + error.message);
const exitCode = Number(error.message.split(" ").pop());
this.exit({
exitCode,
});
} }
} });
// On Exit
this._ptyProcess.onExit((res) => {
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
// Remove room
this.server.io.in(this.name).socketsLeave(this.name);
Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
if (this.callback) {
this.callback(res.exitCode);
}
});
} }
/**
* Exit event handler
* @param res
*/
protected exit = (res : {exitCode: number, signal?: number | undefined}) => {
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
// Remove room
this.server.io.in(this.name).socketsLeave(this.name);
Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
if (this.callback) {
this.callback(res.exitCode);
}
};
public onExit(callback : (exitCode : number) => void) { public onExit(callback : (exitCode : number) => void) {
this.callback = callback; this.callback = callback;
} }

View File

@ -12,11 +12,6 @@ dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export interface LooseObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
let randomBytes : (numBytes: number) => Uint8Array; let randomBytes : (numBytes: number) => Uint8Array;
initRandomBytes(); initRandomBytes();
@ -91,8 +86,8 @@ export const TERMINAL_COLS = 105;
export const TERMINAL_ROWS = 10; export const TERMINAL_ROWS = 10;
export const PROGRESS_TERMINAL_ROWS = 8; export const PROGRESS_TERMINAL_ROWS = 8;
export const COMBINED_TERMINAL_COLS = 58; export const COMBINED_TERMINAL_COLS = 56;
export const COMBINED_TERMINAL_ROWS = 20; export const COMBINED_TERMINAL_ROWS = 15;
export const ERROR_TYPE_VALIDATION = 1; export const ERROR_TYPE_VALIDATION = 1;

View File

@ -6,11 +6,6 @@ import { ERROR_TYPE_VALIDATION } from "./util-common";
import { R } from "redbean-node"; import { R } from "redbean-node";
import { verifyPassword } from "./password-hash"; import { verifyPassword } from "./password-hash";
export interface JWTDecoded {
username : string;
h? : string;
}
export interface DockgeSocket extends Socket { export interface DockgeSocket extends Socket {
userID: number; userID: number;
consoleTerminal? : Terminal; consoleTerminal? : Terminal;

View File

@ -4,14 +4,14 @@
*/ */
export class LimitQueue<T> extends Array<T> { export class LimitQueue<T> extends Array<T> {
__limit; __limit;
__onExceed? : (item : T | undefined) => void; __onExceed = null;
constructor(limit: number) { constructor(limit: number) {
super(); super();
this.__limit = limit; this.__limit = limit;
} }
pushItem(value : T) { push(value : T) {
super.push(value); super.push(value);
if (this.length > this.__limit) { if (this.length > this.__limit) {
const item = this.shift(); const item = this.shift();

View File

@ -7,16 +7,11 @@ services:
# Host Port : Container Port # Host Port : Container Port
- 5001:5001 - 5001:5001
volumes: volumes:
# Docker Socket
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Dockge Config
- ./data:/app/data - ./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) # 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 - /opt/stacks:/opt/stacks
environment: environment:
# Tell Dockge where is your stacks directory # Tell Dockge where is your stacks directory

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
import childProcess from "child_process";
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

View File

@ -1,9 +0,0 @@
// Check if docker is running
import { exec } from "child_process";
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

View File

@ -1,64 +0,0 @@
import pkg from "../package.json";
import childProcess from "child_process";
import fs from "fs";
const newVersion = process.env.VERSION;
console.log("New Version: " + newVersion);
if (! newVersion) {
console.error("invalid version");
process.exit(1);
}
const exists = tagExists(newVersion);
if (! exists) {
// Process package.json
pkg.version = newVersion;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists");
}
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) {
let msg = "Update to " + version;
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error");
}
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version;
}

View File

@ -5,7 +5,7 @@
<li v-for="(value, index) in array" :key="index" class="list-group-item"> <li v-for="(value, index) in array" :key="index" class="list-group-item">
<select v-model="array[index]" class="no-bg domain-input"> <select v-model="array[index]" class="no-bg domain-input">
<option value="">Select a network...</option> <option value="">Select a network...</option>
<option v-for="option in options" :key="option" :value="option">{{ option }}</option> <option v-for="option in options" :value="option">{{ option }}</option>
</select> </select>
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" /> <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />

View File

@ -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 service.ports" :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>
@ -27,7 +27,7 @@
<div v-if="isEditMode" class="mt-2"> <div v-if="isEditMode" class="mt-2">
<button class="btn btn-normal me-2" @click="showConfig = !showConfig"> <button class="btn btn-normal me-2" @click="showConfig = !showConfig">
<font-awesome-icon icon="edit" /> <font-awesome-icon icon="edit" />
{{ $t("Edit") }} Edit
</button> </button>
<button v-if="false" class="btn btn-normal me-2">Rename</button> <button v-if="false" class="btn btn-normal me-2">Rename</button>
<button class="btn btn-danger me-2" @click="remove"> <button class="btn btn-danger me-2" @click="remove">
@ -115,7 +115,7 @@
{{ $tc("network", 2) }} {{ $tc("network", 2) }}
</label> </label>
<div v-if="networkList.length === 0 && service.networks && service.networks.length > 0" class="text-warning mb-3"> <div v-if="networkList.length === 0 && service.networks.length > 0" class="text-warning mb-3">
No networks available. You need to add internal networks or enable external networks in the right side first. No networks available. You need to add internal networks or enable external networks in the right side first.
</div> </div>
@ -179,10 +179,8 @@ export default defineComponent({
}, },
bgStyle() { bgStyle() {
if (this.status === "running" || this.status === "healthy") { if (this.status === "running") {
return "bg-primary"; return "bg-primary";
} else if (this.status === "unhealthy") {
return "bg-danger";
} else { } else {
return "bg-secondary"; return "bg-secondary";
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<h5>{{ $t("Internal Networks") }}</h5> <h5>Internal Networks</h5>
<ul class="list-group"> <ul class="list-group">
<li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item"> <li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item">
<input v-model="networkRow.key" type="text" class="no-bg domain-input" placeholder="Network name..." /> <input v-model="networkRow.key" type="text" class="no-bg domain-input" placeholder="Network name..." />
@ -10,10 +10,10 @@
<button class="btn btn-normal btn-sm mt-3 me-2" @click="addField">{{ $t("addInternalNetwork") }}</button> <button class="btn btn-normal btn-sm mt-3 me-2" @click="addField">{{ $t("addInternalNetwork") }}</button>
<h5 class="mt-3">{{ $t("External Networks") }}</h5> <h5 class="mt-3">External Networks</h5>
<div v-if="externalNetworkList.length === 0"> <div v-if="externalNetworkList.length === 0">
{{ $t("No External Networks") }} No External Networks
</div> </div>
<div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3"> <div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3">
@ -32,7 +32,7 @@
class="form-control" class="form-control"
@keyup.enter="createExternelNetwork" @keyup.enter="createExternelNetwork"
/> />
<button class="btn btn-normal btn-sm me-2" type="button"> <button class="btn btn-normal btn-sm me-2" type="button" @click="">
{{ $t("createExternalNetwork") }} {{ $t("createExternalNetwork") }}
</button> </button>
</div> </div>

View File

@ -77,8 +77,8 @@ export default {
} }
this.terminal = new Terminal({ this.terminal = new Terminal({
fontSize: 14, fontSize: 16,
fontFamily: "'JetBrains Mono', monospace", fontFamily: "monospace",
cursorBlink, cursorBlink,
cols: this.cols, cols: this.cols,
rows: this.rows, rows: this.rows,

View File

@ -19,6 +19,7 @@ export default {
computed: { computed: {
uptime() { uptime() {
return "0.00%";
return this.$t("notAvailableShort"); return this.$t("notAvailableShort");
}, },

View File

@ -47,10 +47,10 @@
<input <input
v-model="settings.primaryHostname" v-model="settings.primaryHostname"
class="form-control" class="form-control"
placeholder="(Unset: Follow current hostname)" placeholder="localhost"
/> />
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryHostname"> <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryHostname">
{{ $t("autoGet") }} {{ $t("Auto Get") }}
</button> </button>
</div> </div>
@ -68,13 +68,13 @@
</template> </template>
<script> <script>
import HiddenInput from "../../components/HiddenInput.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { timezoneList } from "../../util-frontend"; import { timezoneList } from "../../util-frontend";
export default { export default {
components: { components: {
HiddenInput,
}, },
data() { data() {

View File

@ -3,16 +3,7 @@ import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
import en from "./lang/en.json"; import en from "./lang/en.json";
const languageList = { const languageList = {
"bg-BG": "Български",
"es": "Español",
"de": "Deutsch",
"fr": "Français",
"pt": "Português",
"tr": "Türkçe",
"zh-CN": "简体中文",
"ur": "Urdu",
"ko-KR": "한국어",
"ru": "Русский",
}; };
let messages = { let messages = {

View File

@ -1,14 +0,0 @@
# Translations
A simple guide on how to translate `Dockge` in your native language.
## How to add a new language in the dropdown
(11-21-2023) Updated
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": "繁體中文 (台灣)"`,
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.

View File

@ -1,94 +0,0 @@
{
"languageName": "Български",
"Create your admin account": "Създайте администраторски профил",
"authIncorrectCreds": "Грешно име или парола.",
"PasswordsDoNotMatch": "Паролите не съвпадат.",
"Repeat Password": "Повторете паролата",
"Create": "Създай",
"signedInDisp": "Вписан като {0}",
"signedInDispDisabled": "Удостоверяването е изключено.",
"home": "Начало",
"console": "Конзола",
"registry": "Регистър",
"compose": "Compose",
"addFirstStackMsg": "Създайте вашия първи стак!",
"stackName" : "Име на стак",
"deployStack": "Разположи",
"deleteStack": "Изтрий",
"stopStack": "Спри",
"restartStack": "Рестартирай",
"updateStack": "Актуализирай",
"startStack": "Стартирай",
"editStack": "Редактирай",
"discardStack": "Отхвърли",
"saveStackDraft": "Запази",
"notAvailableShort" : "N/A",
"deleteStackMsg": "Сигурни ли сте, че желаете да изтриете този стак?",
"stackNotManagedByDockgeMsg": "Този стак не се управлява от Dockge.",
"primaryHostname": "Основно име на хост",
"general": "Общи",
"container": "Контейнер | Контейнери",
"scanFolder": "Сканиране папката със стакове",
"dockerImage": "Изображение",
"restartPolicyUnlessStopped": "Докато не бъде спрян",
"restartPolicyAlways": "Винаги",
"restartPolicyOnFailure": "При неуспех",
"restartPolicyNo": "Не",
"environmentVariable": "Променлива на средата | Променливи на средата",
"restartPolicy": "Правила за рестартиране",
"containerName": "Име на контейнер",
"port": "Порт | Портове",
"volume": "Том | Томове",
"network": "Мрежа | Мрежи",
"dependsOn": "Зависимост от контейнер | Зависимост от контейнери",
"addListItem": "Добави {0}",
"deleteContainer": "Изтрий",
"addContainer": "Добави контейнер",
"addNetwork": "Добави мрежа",
"disableauth.message1": "Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?",
"disableauth.message2": "Използва се в случаите, <strong>когато има настроен алтернативен метод за удостоверяване</strong> преди Dockge, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.",
"passwordNotMatchMsg": "Повторената парола не съвпада.",
"autoGet": "Автоматично получаване",
"add": "Добави",
"Edit": "Редактирай",
"applyToYAML": "Приложи към YAML",
"createExternalNetwork": "Създай",
"addInternalNetwork": "Добави",
"Save": "Запиши",
"Language": "Език",
"Current User": "Текущ потребител",
"Change Password": "Промени парола",
"Current Password": "Текуща парола",
"New Password": "Нова парола",
"Repeat New Password": "Повторете новата парола",
"Update Password": "Актуализирай парола",
"Advanced": "Разширени",
"Please use this option carefully!": "Моля, използвайте с повишено внимание!",
"Enable Auth": "Включи удостоверяване",
"Disable Auth": "Изключи удостоверяване",
"I understand, please disable": "Разбирам. Моля, изключи",
"Leave": "Напусни",
"Frontend Version": "Фронтенд версия",
"Check Update On GitHub": "Проверка за актуализация в GitHub",
"Show update if available": "Покажи актуализация, ако е налична",
"Also check beta release": "Проверявай и за бета версии",
"Remember me": "Запомни ме",
"Login": "Вписване",
"Username": "Потребител",
"Password": "Парола",
"Settings": "Настройки",
"Logout": "Изход",
"Lowercase only": "Само малки букви",
"Convert to Compose": "Конвертирай в \"Compose\" формат",
"Docker Run": "Стартирай Docker",
"active": "активен",
"exited": "излязъл",
"inactive": "неактивен",
"Appearance": "Изглед",
"Security": "Сигурност",
"About": "Относно",
"Allowed commands:": "Позволени команди:",
"Internal Networks": "Вътрешни мрежи",
"External Networks": "Външни мрежи",
"No External Networks": "Не са налични външни мрежи"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "Deutsch",
"Create your admin account": "Erstelle dein Admin-Konto",
"authIncorrectCreds": "Falscher Benutzername oder falsches Passwort.",
"PasswordsDoNotMatch": "Passwörter stimmen nicht überein.",
"Repeat Password": "Passwort wiederholen",
"Create": "Erstellen",
"signedInDisp": "Angemeldet als {0}",
"signedInDispDisabled": "Authentifizierung deaktiviert.",
"home": "Startseite",
"console": "Konsole",
"registry": "Register",
"compose": "Zusammenstellen",
"addFirstStackMsg": "Stelle deinen ersten Stack zusammen!",
"stackName" : "Stack-Name",
"deployStack": "Bereitstellen",
"deleteStack": "Löschen",
"stopStack": "Anhalten",
"restartStack": "Neustarten",
"updateStack": "Aktualisieren",
"startStack": "Starten",
"editStack": "Bearbeiten",
"discardStack": "Verwerfen",
"saveStackDraft": "Speichern",
"notAvailableShort" : "N/A",
"deleteStackMsg": "Möchtest du diesen Stack wirklich löschen?",
"stackNotManagedByDockgeMsg": "Dieser Stack wird nicht von Dockge verwaltet.",
"primaryHostname": "Primärer Hostname",
"general": "Allgemein",
"container": "Container | Container",
"scanFolder": "Stacks-Ordner durchsuchen",
"dockerImage": "Image",
"restartPolicyUnlessStopped": "Falls nicht gestoppt",
"restartPolicyAlways": "Immer",
"restartPolicyOnFailure": "Bei Fehler",
"restartPolicyNo": "Kein Neustart",
"environmentVariable": "Umgebungsvariable | Umgebungsvariablen",
"restartPolicy": "Neustart Richtlinie",
"containerName": "Container-Name",
"port": "Port | Ports",
"volume": "Volume | Volumes",
"network": "Netzwerk | Netzwerke",
"dependsOn": "Container-Abhängigkeit | Container-Abhängigkeiten",
"addListItem": "{0} hinzufügen",
"deleteContainer": "Löschen",
"addContainer": "Container hinzufügen",
"addNetwork": "Netzwerk hinzufügen",
"disableauth.message1": "Bist du sicher, dass du die <strong>Authentifizierung 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.",
"passwordNotMatchMsg": "Das wiederholte Passwort stimmt nicht überein.",
"autoGet": "Automatisch holen",
"add": "Hinzufügen",
"Edit": "Bearbeiten",
"applyToYAML": "Auf YAML anwenden",
"createExternalNetwork": "Erstellen",
"addInternalNetwork": "Hinzufügen",
"Save": "Speichern",
"Language": "Sprache",
"Current User": "Aktueller Benutzer",
"Change Password": "Passwort ändern",
"Current Password": "Aktuelles Passwort",
"New Password": "Neues Passwort",
"Repeat New Password": "Neues Passwort wiederholen",
"Update Password": "Passwort aktualisieren",
"Advanced": "Erweitert",
"Please use this option carefully!": "Bitte verwende diese Option sorgfältig!",
"Enable Auth": "Authentifizierung aktivieren",
"Disable Auth": "Authentifizierung deaktivieren",
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
"Leave": "Verlassen",
"Frontend Version": "Frontend Version",
"Check Update On GitHub": "Update auf GitHub überprüfen",
"Show update if available": "Update anzeigen, wenn verfügbar",
"Also check beta release": "Auch Beta-Version überprüfen",
"Remember me": "Anmeldung beibehalten",
"Login": "Anmelden",
"Username": "Benutzername",
"Password": "Passwort",
"Settings": "Einstellungen",
"Logout": "Abmelden",
"Lowercase only": "Nur Kleinbuchstaben",
"Convert to Compose": "In Compose Syntax umwandeln",
"Docker Run": "Docker ausführen",
"active": "aktiv",
"exited": "beendet",
"inactive": "inaktiv",
"Appearance": "Erscheinungsbild",
"Security": "Sicherheit",
"About": "Über",
"Allowed commands:": "Zugelassene Befehle:",
"Internal Networks": "Interne Netzwerke",
"External Networks": "Externe Netzwerke",
"No External Networks": "Keine externen Netzwerke"
}

View File

@ -1,10 +1,7 @@
{ {
"languageName": "English", "languageName": "English",
"Create your admin account": "Create your admin account",
"authIncorrectCreds": "Incorrect username or password.", "authIncorrectCreds": "Incorrect username or password.",
"PasswordsDoNotMatch": "Passwords do not match.", "PasswordsDoNotMatch": "Passwords do not match.",
"Repeat Password": "Repeat Password",
"Create": "Create",
"signedInDisp": "Signed in as {0}", "signedInDisp": "Signed in as {0}",
"signedInDispDisabled": "Auth Disabled.", "signedInDispDisabled": "Auth Disabled.",
"home": "Home", "home": "Home",
@ -46,49 +43,11 @@
"addContainer": "Add Container", "addContainer": "Add Container",
"addNetwork": "Add Network", "addNetwork": "Add Network",
"disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?", "disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
"disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Dockge such as Cloudflare Access, Authelia or other authentication mechanisms.", "disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
"passwordNotMatchMsg": "The repeat password does not match.", "passwordNotMatchMsg": "The repeat password does not match.",
"autoGet": "Auto Get", "autoGet": "Auto Get",
"add": "Add", "add": "Add",
"Edit": "Edit",
"applyToYAML": "Apply to YAML", "applyToYAML": "Apply to YAML",
"createExternalNetwork": "Create", "createExternalNetwork": "Create",
"addInternalNetwork": "Add", "addInternalNetwork": "Add"
"Save": "Save",
"Language": "Language",
"Current User": "Current User",
"Change Password": "Change Password",
"Current Password": "Current Password",
"New Password": "New Password",
"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": "Check Update On GitHub",
"Show update if available": "Show update if available",
"Also check beta release": "Also check beta release",
"Remember me": "Remember me",
"Login": "Login",
"Username": "Username",
"Password": "Password",
"Settings": "Settings",
"Logout": "Logout",
"Lowercase only": "Lowercase only",
"Convert to Compose": "Convert to Compose",
"Docker Run": "Docker Run",
"active": "active",
"exited": "exited",
"inactive": "inactive",
"Appearance": "Appearance",
"Security": "Security",
"About": "About",
"Allowed commands:": "Allowed commands:",
"Internal Networks": "Internal Networks",
"External Networks": "External Networks",
"No External Networks": "No External Networks"
} }

View File

@ -1,94 +0,0 @@
{
"languageName": "Español",
"Create your admin account": "Crea tu cuenta de administrador",
"authIncorrectCreds": "Nombre de usuario o contraseña incorrectos.",
"PasswordsDoNotMatch": "Las contraseñas no coinciden.",
"Repeat Password": "Repetir Contraseña",
"Create": "Crear",
"signedInDisp": "Sesión iniciada como {0}",
"signedInDispDisabled": "Autenticación deshabilitada.",
"home": "Inicio",
"console": "Consola",
"registry": "Registro",
"compose": "Componer",
"addFirstStackMsg": "¡Compón tu primera pila!",
"stackName" : "Nombre de la Pila",
"deployStack": "Desplegar",
"deleteStack": "Eliminar",
"stopStack": "Detener",
"restartStack": "Reiniciar",
"updateStack": "Actualizar",
"startStack": "Iniciar",
"editStack": "Editar",
"discardStack": "Descartar",
"saveStackDraft": "Guardar",
"notAvailableShort" : "N/D",
"deleteStackMsg": "¿Estás seguro de que quieres eliminar esta pila?",
"stackNotManagedByDockgeMsg": "Esta pila no está gestionada por Dockge.",
"primaryHostname": "Nombre de Host Primario",
"general": "General",
"container": "Contenedor | Contenedores",
"scanFolder": "Escanear Carpeta de Pilas",
"dockerImage": "Imagen",
"restartPolicyUnlessStopped": "A menos que se detenga",
"restartPolicyAlways": "Siempre",
"restartPolicyOnFailure": "En caso de fallo",
"restartPolicyNo": "No",
"environmentVariable": "Variable de Entorno | Variables de Entorno",
"restartPolicy": "Política de Reinicio",
"containerName": "Nombre del Contenedor",
"port": "Puerto | Puertos",
"volume": "Volumen | Volúmenes",
"network": "Red | Redes",
"dependsOn": "Dependencia del Contenedor | Dependencias del Contenedor",
"addListItem": "Agregar {0}",
"deleteContainer": "Eliminar",
"addContainer": "Agregar Contenedor",
"addNetwork": "Agregar Red",
"disableauth.message1": "¿Estás seguro de que deseas <strong>desactivar la autenticación</strong>?",
"disableauth.message2": "Está diseñado para escenarios <strong>donde pretendes implementar autenticación de terceros</strong> frente a Dockge, como Cloudflare Access, Authelia u otros mecanismos de autenticación.",
"passwordNotMatchMsg": "La contraseña repetida no coincide.",
"autoGet": "Obtener Automáticamente",
"add": "Agregar",
"Edit": "Editar",
"applyToYAML": "Aplicar a YAML",
"createExternalNetwork": "Crear",
"addInternalNetwork": "Agregar",
"Save": "Guardar",
"Language": "Idioma",
"Current User": "Usuario Actual",
"Change Password": "Cambiar Contraseña",
"Current Password": "Contraseña Actual",
"New Password": "Nueva Contraseña",
"Repeat New Password": "Repetir Nueva Contraseña",
"Update Password": "Actualizar Contraseña",
"Advanced": "Avanzado",
"Please use this option carefully!": "¡Por favor, usa esta opción con cuidado!",
"Enable Auth": "Habilitar Autenticación",
"Disable Auth": "Deshabilitar Autenticación",
"I understand, please disable": "Entiendo, por favor deshabilitar",
"Leave": "Salir",
"Frontend Version": "Versión del Frontend",
"Check Update On GitHub": "Comprobar Actualización en GitHub",
"Show update if available": "Mostrar actualización si está disponible",
"Also check beta release": "También verificar la versión beta",
"Remember me": "Recuérdame",
"Login": "Iniciar Sesión",
"Username": "Nombre de Usuario",
"Password": "Contraseña",
"Settings": "Configuración",
"Logout": "Cerrar Sesión",
"Lowercase only": "Solo minúsculas",
"Convert to Compose": "Convertir a Compose",
"Docker Run": "Ejecutar Docker",
"active": "activo",
"exited": "finalizado",
"inactive": "inactivo",
"Appearance": "Apariencia",
"Security": "Seguridad",
"About": "Acerca de",
"Allowed commands:": "Comandos permitidos:",
"Internal Networks": "Redes Internas",
"External Networks": "Redes Externas",
"No External Networks": "Sin Redes Externas"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "Francais",
"Create your admin account": "Créez votre compte administrateur",
"authIncorrectCreds": "identifiant ou mot de passe incorrect.",
"Repeat Password": "Répéter le mot de passe",
"PasswordsDoNotMatch": "Les mots de passe ne correspondent pas.",
"Create": "Créer",
"signedInDisp": "Connecté en tant que {0}",
"signedInDispDisabled": "Authentification désactivée.",
"home": "Accueil",
"console": "Console",
"registry": "Registre",
"compose": "Compose",
"addFirstStackMsg": "Créez votre première pile!",
"stackName" : "Nom de la pile",
"deployStack": "Déployer",
"deleteStack": "Supprimer",
"stopStack": "Arrêter",
"restartStack": "Redémarrer",
"updateStack": "Mettre à jour",
"startStack": "Démarrer",
"editStack": "Modifier",
"discardStack": "Ignorer",
"saveStackDraft": "Sauvegarder",
"notAvailableShort" : "N/A",
"deleteStackMsg": "Êtes-vous sûr de vouloir supprimer cette pile ?",
"stackNotManagedByDockgeMsg": "Cette pile n'est pas gérée par Dockge.",
"primaryHostname": "Nom d'hôte principal",
"general": "Générale",
"container": "Conteneur | Conteneurs",
"scanFolder": "Analyser le dossier des piles",
"dockerImage": "Image",
"restartPolicyUnlessStopped": "Sauf arrêt",
"restartPolicyAlways": "Toujours",
"restartPolicyOnFailure": "En cas d'échec",
"restartPolicyNo": "Non",
"environmentVariable": "Variable d'environnement | Variables d'environnement",
"restartPolicy": "Politique de redémarrage",
"containerName": "Nom du conteneur",
"port": "Port | Ports",
"volume": "Volume | Volumes",
"network": "Réseau | Réseaux",
"dependsOn": "Dépendance du conteneur | Dépendances du conteneur",
"addListItem": "Ajouter {0}",
"deleteContainer": "Supprimer",
"addContainer": "Ajouter un conteneur",
"addNetwork": "Ajouter un réseau",
"disableauth.message1": "Voulez-vous vraiment <strong>désactiver l'authentification</strong> ?",
"disableauth.message2": "Il est conçu pour les scénarios <strong>dans lesquels vous avez l'intention d'implémenter une authentification tierce</strong> devant Dockge, comme Cloudflare Access, Authelia ou d'autres mécanismes d'authentification.",
"passwordNotMatchMsg": "Le mot de passe de confirmation ne correspond pas.",
"autoGet": "Obtention automatique",
"add": "Ajouter",
"Edit": "Modifier",
"applyToYAML": "Appliquer à YAML",
"createExternalNetwork": "Créer",
"addInternalNetwork": "Ajouter",
"Save": "Enregistrer",
"Language": "Langue",
"Current User": "Utilisateur Actuel",
"Change Password": "Changer le Mot de Passe",
"Current Password": "Mot de passe actuel",
"New Password": "Nouveau Mot de Passe",
"Repeat New Password": "Répéter le Nouveau Mot de Passe",
"Update Password": "Mettre à Jour le Mot de Passe",
"Advanced": "Avancé",
"Please use this option carefully!": "Veuillez utiliser cette option avec précaution !",
"Enable Auth": "Activer l'Authentification",
"Disable Auth": "Désactiver l'Authentification",
"I understand, please disable": "Je comprends, veuillez désactiver",
"Leave": "Quitter",
"Frontend Version": "Version Frontend",
"Check Update On GitHub": "Vérifier la Mise à Jour sur GitHub",
"Show update if available": "Afficher la mise à jour si disponible",
"Also check beta release": "Vérifier également la version bêta",
"Remember me": "Se souvenir de moi",
"Login": "Connexion",
"Username": "Nom d'utilisateur",
"Password": "Mot de Passe",
"Settings": "Paramètres",
"Logout": "Déconnexion",
"Lowercase only": "Minuscules uniquement",
"Convert to Compose": "Convertir en Compose",
"Docker Run": "Exécution Docker",
"active": "actif",
"exited": "arrêté",
"inactive": "inactif",
"Appearance": "Apparence",
"Security": "Sécurité",
"About": "À propos",
"Allowed commands:": "Commandes autorisées:",
"Internal Networks": "Réseaux Internes",
"External Networks": "Réseaux Externes",
"No External Networks": "Aucun Réseau Externe"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "한국어",
"Create your admin account": "관리자 계정 만들기",
"authIncorrectCreds": "사용자명 또는 비밀번호가 일치하지 않아요.",
"PasswordsDoNotMatch": "비밀번호가 일치하지 않아요.",
"Repeat Password": "비밀번호 재입력",
"Create": "생성",
"signedInDisp": "{0}(으)로 로그인됨",
"signedInDispDisabled": "인증 비활성화됨.",
"home": "홈",
"console": "콘솔",
"registry": "레지스트리",
"compose": "생성",
"addFirstStackMsg": "첫 번째 스택을 만들어 보세요!",
"stackName": "스택 이름",
"deployStack": "배포",
"deleteStack": "삭제",
"stopStack": "정지",
"restartStack": "재시작",
"updateStack": "업데이트",
"startStack": "시작",
"editStack": "수정",
"discardStack": "취소",
"saveStackDraft": "저장",
"notAvailableShort": "N/A",
"deleteStackMsg": "정말로 이 스택을 삭제하시겠습니까?",
"stackNotManagedByDockgeMsg": "이 스택은 Dockge에 의해 관리되지 않아요.",
"primaryHostname": "주 호스트명",
"general": "일반",
"container": "컨테이너",
"scanFolder": "스택 폴더 스캔",
"dockerImage": "이미지",
"restartPolicyUnlessStopped": "종료되기 전까지",
"restartPolicyAlways": "항상",
"restartPolicyOnFailure": "오류 발생 시",
"restartPolicyNo": "안 함",
"environmentVariable": "환경 변수",
"restartPolicy": "재시작 정책",
"containerName": "컨테이너 이름",
"port": "포트",
"volume": "볼륨",
"network": "네트워크",
"dependsOn": "컨테이너 의존성",
"addListItem": "{0} 추가",
"deleteContainer": "삭제",
"addContainer": "컨테이너 추가",
"addNetwork": "네트워크 추가",
"disableauth.message1": "정말로 <strong>인증을 비활성화</strong>하시겠습니까?",
"disableauth.message2": "이 기능은 Dockge 앞에 Cloudflare Access, Authelia 등과 같은 <strong>서드 파티 인증을 사용하려는 경우</strong>에 사용하기 위해서 만들어졌어요.",
"passwordNotMatchMsg": "비밀번호 재입력이 일치하지 않아요..",
"autoGet": "자동으로 가져오기",
"add": "추가",
"Edit": "수정",
"applyToYAML": "YAML에 적용",
"createExternalNetwork": "생성",
"addInternalNetwork": "추가",
"Save": "저장",
"Language": "언어",
"Current User": "현재 사용자",
"Change Password": "비밀번호 변경",
"Current Password": "현재 비밀번호",
"New Password": "새 비밀번호",
"Repeat New Password": "새 비밀번호 재입력",
"Update Password": "비밀번호 변경",
"Advanced": "고급",
"Please use this option carefully!": "이 설정은 신중히 사용하세요!",
"Enable Auth": "인증 활성화",
"Disable Auth": "인증 비활성화",
"I understand, please disable": "이해하고 있습니다. 비활성화해 주세요",
"Leave": "취소",
"Frontend Version": "프론트엔드 버전",
"Check Update On GitHub": "GitHub에서 업데이트 확인",
"Show update if available": "업데이트가 있을 때 표시",
"Also check beta release": "베타 버전도 확인",
"Remember me": "기억하기",
"Login": "로그인",
"Username": "사용자명",
"Password": "비밀번호",
"Settings": "설정",
"Logout": "로그아웃",
"Lowercase only": "소문자만",
"Convert to Compose": "Compose로 변환",
"Docker Run": "Docker Run",
"active": "활성",
"exited": "종료됨",
"inactive": "비활성",
"Appearance": "디스플레이",
"Security": "보안",
"About": "정보",
"Allowed commands:": "허용된 명령어:",
"Internal Networks": "내부 네트워크",
"External Networks": "외부 네트워크",
"No External Networks": "외부 네트워크 없음"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "Português",
"Create your admin account": "Crie sua conta de administrador",
"authIncorrectCreds": "Nome de usuário ou senha incorretos.",
"PasswordsDoNotMatch": "As senhas não coincidem.",
"Repeat Password": "Repetir Senha",
"Create": "Criar",
"signedInDisp": "Logado como {0}",
"signedInDispDisabled": "Autenticação desativada.",
"home": "Início",
"console": "Console",
"registry": "Registro",
"compose": "Compor",
"addFirstStackMsg": "Componha sua primeira pilha!",
"stackName" : "Nome da Pilha",
"deployStack": "Implantar",
"deleteStack": "Excluir",
"stopStack": "Parar",
"restartStack": "Reiniciar",
"updateStack": "Atualizar",
"startStack": "Iniciar",
"editStack": "Editar",
"discardStack": "Descartar",
"saveStackDraft": "Salvar",
"notAvailableShort" : "N/D",
"deleteStackMsg": "Tem certeza de que deseja excluir esta pilha?",
"stackNotManagedByDockgeMsg": "Esta pilha não é gerenciada pelo Dockge.",
"primaryHostname": "Nome do Host Primário",
"general": "Geral",
"container": "Contêiner | Contêineres",
"scanFolder": "Digitalizar Pasta de Pilhas",
"dockerImage": "Imagem",
"restartPolicyUnlessStopped": "A menos que seja parado",
"restartPolicyAlways": "Sempre",
"restartPolicyOnFailure": "Em caso de falha",
"restartPolicyNo": "Não",
"environmentVariable": "Variável de Ambiente | Variáveis de Ambiente",
"restartPolicy": "Política de Reinicialização",
"containerName": "Nome do Contêiner",
"port": "Porta | Portas",
"volume": "Volume | Volumes",
"network": "Rede | Redes",
"dependsOn": "Dependência do Contêiner | Dependências do Contêiner",
"addListItem": "Adicionar {0}",
"deleteContainer": "Excluir",
"addContainer": "Adicionar Contêiner",
"addNetwork": "Adicionar Rede",
"disableauth.message1": "Tem certeza de que deseja <strong>desativar a autenticação</strong>?",
"disableauth.message2": "Isso é projetado para cenários <strong>onde você pretende implementar autenticação de terceiros</strong> no Dockge, como Cloudflare Access, Authelia ou outros mecanismos de autenticação.",
"passwordNotMatchMsg": "A senha repetida não coincide.",
"autoGet": "Obter Automaticamente",
"add": "Adicionar",
"Edit": "Editar",
"applyToYAML": "Aplicar ao YAML",
"createExternalNetwork": "Criar",
"addInternalNetwork": "Adicionar",
"Save": "Salvar",
"Language": "Idioma",
"Current User": "Usuário Atual",
"Change Password": "Alterar Senha",
"Current Password": "Senha Atual",
"New Password": "Nova Senha",
"Repeat New Password": "Repetir Nova Senha",
"Update Password": "Atualizar Senha",
"Advanced": "Avançado",
"Please use this option carefully!": "Por favor, use esta opção com cuidado!",
"Enable Auth": "Habilitar Autenticação",
"Disable Auth": "Desabilitar Autenticação",
"I understand, please disable": "Entendo, por favor desabilitar",
"Leave": "Sair",
"Frontend Version": "Versão da Interface",
"Check Update On GitHub": "Verificar Atualização no GitHub",
"Show update if available": "Mostrar atualização se disponível",
"Also check beta release": "Também verificar versão beta",
"Remember me": "Lembrar-me",
"Login": "Entrar",
"Username": "Nome de Usuário",
"Password": "Senha",
"Settings": "Configurações",
"Logout": "Sair",
"Lowercase only": "Somente minúsculas",
"Convert to Compose": "Converter para Compose",
"Docker Run": "Executar Docker",
"active": "ativo",
"exited": "encerrado",
"inactive": "inativo",
"Appearance": "Aparência",
"Security": "Segurança",
"About": "Sobre",
"Allowed commands:": "Comandos permitidos:",
"Internal Networks": "Redes Internas",
"External Networks": "Redes Externas",
"No External Networks": "Sem Redes Externas"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "Русский",
"Create your admin account": "Создайте учетку администратора",
"authIncorrectCreds": "Неверный логин или пароль.",
"PasswordsDoNotMatch": "Пароль не совпадает.",
"Repeat Password": "Повторите пароль",
"Create": "Создать",
"signedInDisp": "Авторизлван как {0}",
"signedInDispDisabled": "Авторизация выключена.",
"home": "Главная",
"console": "Консоль",
"registry": "Registry",
"compose": "Compose",
"addFirstStackMsg": "Создайте свой первый стек!",
"stackName" : "Имя стека",
"deployStack": "Развернуть",
"deleteStack": "Удалить",
"stopStack": "Остановить",
"restartStack": "Перезапустить",
"updateStack": "Обновить",
"startStack": "Запустить",
"editStack": "Изменить",
"discardStack": "Отменить",
"saveStackDraft": "Сохранить",
"notAvailableShort" : "Н/Д",
"deleteStackMsg": "Вы уверены что хотите удалить этот стек?",
"stackNotManagedByDockgeMsg": "Данный стек не обслуживается Dockge.",
"primaryHostname": "Имя хоста",
"general": "Главное",
"container": "Контейнер | Контейнеры",
"scanFolder": "Сканировать папку стеков",
"dockerImage": "Образ",
"restartPolicyUnlessStopped": "Пока не будет остановлен",
"restartPolicyAlways": "Всегда",
"restartPolicyOnFailure": "При падении",
"restartPolicyNo": "Никогда",
"environmentVariable": "Переменная окружения | Переменные окружения",
"restartPolicy": "Политика рестарта",
"containerName": "Имя контейнера",
"port": "Порт | Порты",
"volume": "Хранилище | Хранилища",
"network": "Сеть | Сети",
"dependsOn": "Зависимость контейнера | Зависимости контейнера",
"addListItem": "Добавить {0}",
"deleteContainer": "Удалить",
"addContainer": "Добавить Контейнер",
"addNetwork": "Добавить Сеть",
"disableauth.message1": "Вы уверены что хотите <strong>выключить авторизацию</strong>?",
"disableauth.message2": "Он предназначен для сценариев, <strong>где вы собираетесь реализовать стороннюю аутентификацию</strong> перед Dockge, например Cloudflare Access, Authelia или другие механизмы аутентификации.",
"passwordNotMatchMsg": "Повторный пароль не совпадает.",
"autoGet": "Auto Get",
"add": "Добавить",
"Edit": "Изменить",
"applyToYAML": "Применить к YAML",
"createExternalNetwork": "Создать",
"addInternalNetwork": "Добавить",
"Save": "Сохранить",
"Language": "Язык",
"Current User": "Текущий пользователь",
"Change Password": "Изменить пароль",
"Current Password": "Текущий пароль",
"New Password": "Новый пароль",
"Repeat New Password": "Повторите новый пароль",
"Update Password": "Обновить пароль",
"Advanced": "Продвинутые опции",
"Please use this option carefully!": "Пожалуйста, используйте эту опцию осторожно!",
"Enable Auth": "Включить аутентификацию",
"Disable Auth": "Отключить аутентификацию",
"I understand, please disable": "Я понимаю, пожалуйста, отключите",
"Leave": "Покинуть",
"Frontend Version": "Версия внешнего интерфейса",
"Check Update On GitHub": "Проверьте обновление на GitHub",
"Show update if available": "Показать обновление, если оно доступно",
"Also check beta release": "Также проверьте бета-версию",
"Remember me": "Запомнить меня",
"Login": "Логин",
"Username": "Имя пользователя",
"Password": "Пароль",
"Settings": "Настройки",
"Logout": "Выйти",
"Lowercase only": "Только нижний регистр",
"Convert to Compose": "Преобразовать вCompose",
"Docker Run": "Запустить Docker",
"active": "активный",
"exited": "завершенный",
"inactive": "неактинвый",
"Appearance": "Внешний вид",
"Security": "Безопасность",
"About": "О продукте",
"Allowed commands:": "Разрешенные команды:",
"Internal Networks": "Внутренние сети",
"External Networks": "Внешние сети",
"No External Networks": "Нет внешних сетей"
}

View File

@ -1,53 +0,0 @@
{
"languageName": "Türkçe",
"authIncorrectCreds": "Yanlış kullanıcı adı veya parola.",
"PasswordsDoNotMatch": "Parolalar eşleşmiyor.",
"signedInDisp": "{0} olarak oturum açıldı",
"signedInDispDisabled": "Yetkilendirme Devre Dışı.",
"home": "Anasayfa",
"console": "Konsol",
"registry": "Kayıt Defteri",
"compose": "Compose",
"addFirstStackMsg": "İlk yığınınızı oluşturun!",
"stackName" : "Yığın Adı",
"deployStack": "Dağıtmak",
"deleteStack": "Sil",
"stopStack": "Dudur",
"restartStack": "Yeniden Başlat",
"updateStack": "Güncelle",
"startStack": "Başlat",
"editStack": "Düzenle",
"discardStack": ıkar",
"saveStackDraft": "Kaydet",
"notAvailableShort" : "N/A",
"deleteStackMsg": "Bu yığını silmek istediğinizden emin misiniz?",
"stackNotManagedByDockgeMsg": "Bu yığın Dockge tarafından yönetilmemektedir.",
"primaryHostname": "Birincil Ana Bilgisayar Adı",
"general": "Genel",
"container": "Konteyner | Konteynerler",
"scanFolder": "Yığınlar Klasörünü Tara",
"dockerImage": "Görüntü",
"restartPolicyUnlessStopped": "Durdurulana Kadar",
"restartPolicyAlways": "Her zaman",
"restartPolicyOnFailure": "Başarısızlıkta",
"restartPolicyNo": "Hayır",
"environmentVariable": "Ortam Değişkeni | Ortam Değişkenleri",
"restartPolicy": "Yeniden Başlatma Politikası",
"containerName": "Konteyner Adı",
"port": "Port | Portlar",
"volume": "Disk Bölümü | Disk Bölümleri",
"network": "Ağ | Ağlar",
"dependsOn": "Konteyner Bağımlılığı | Konteyner Bağımlılıkları",
"addListItem": "{0} Ekle",
"deleteContainer": "Sil",
"addContainer": "Konteyner Ekle",
"addNetwork": "Ağ Ekle",
"disableauth.message1": "<strong>Kimlik doğrulamayı devre dışı</strong> bırakmak istediğinizden emin misiniz?",
"disableauth.message2": "Cloudflare Access, Authelia veya diğer kimlik doğrulama mekanizmaları gibi Uptime Kuma'nın önünde <strong>üçüncü taraf kimlik doğrulaması uygulamak</strong> istediğiniz senaryolar için tasarlanmıştır.",
"passwordNotMatchMsg": "Tekrarlanan parola eşleşmiyor.",
"autoGet": "Otomatik Al",
"add": "Ekle",
"applyToYAML": "YAML'ye uygulayın",
"createExternalNetwork": "Oluştur",
"addInternalNetwork": "Ekle"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "اردو",
"Create your admin account": "اپنا ایڈمن اکاؤنٹ بنائیں",
"authIncorrectCreds": "غلط صارف نام یا پاس ورڈ.",
"PasswordsDoNotMatch": "پاس ورڈز کوئی مماثل نہیں ہیں۔",
"Repeat Password": "پاس ورڈ دوبارہ لکھیے",
"Create": "بنانا",
"signedInDisp": "بطور {0} سائن ان",
"signedInDispDisabled": "توثیق غیر فعال۔",
"home": "گھر",
"console": "تسلی",
"registry": "رجسٹری",
"compose": "تحریر",
"addFirstStackMsg": "اپنا پہلا اسٹیک کمپوز کریں!",
"stackName" : "اسٹیک کا نام",
"deployStack": "تعینات",
"deleteStack": "حذف کریں",
"stopStack": "روکو",
"restartStack": "دوبارہ شروع کریں",
"updateStack": "اپ ڈیٹ",
"startStack": "شروع کریں۔",
"editStack": "ترمیم",
"discardStack": "رد کر دیں۔",
"saveStackDraft": "محفوظ کریں۔",
"notAvailableShort" : "N / A",
"deleteStackMsg": "کیا آپ واقعی اس اسٹیک کو حذف کرنا چاہتے ہیں؟",
"stackNotManagedByDockgeMsg": "یہ اسٹیک Dockge کے زیر انتظام نہیں ہے۔",
"primaryHostname": "بنیادی میزبان نام",
"general": "جنرل",
"container": "کنٹینر | کنٹینرز",
"scanFolder": "اسٹیک فولڈر کو اسکین کریں۔",
"dockerImage": "تصویر",
"restartPolicyUnlessStopped": "جب تک روکا نہیں جاتا",
"restartPolicyAlways": "ہمیشہ",
"restartPolicyOnFailure": "ناکامی پر",
"restartPolicyNo": "نہیں",
"environmentVariable": "ماحولیاتی متغیر | ماحولیاتی تغیرات",
"restartPolicy": "پالیسی کو دوبارہ شروع کریں",
"containerName": "کنٹینر کا نام",
"port": "پورٹ | بندرگاہیں",
"volume": "والیوم | جلدیں",
"network": "نیٹ ورک | نیٹ ورکس",
"dependsOn": "کنٹینر انحصار | کنٹینر انحصار",
"addListItem": "شامل کریں {0}",
"deleteContainer": "حذف کریں",
"addContainer": "کنٹینر شامل کریں",
"addNetwork": "نیٹ ورک شامل کریں",
"disableauth.message1": "کیا آپ واقعی <strong>تصدیق کو غیر فعال</strong> کرنا چاہتے ہیں؟",
"disableauth.message2": "یہ ان منظرناموں کے لیے ڈیزائن کیا گیا ہے جہاں <strong>آپ کا ارادہ ہے تیسرے فریق کی توثیق کو لاگو کرنے کا</strong> Dockge کے سامنے جیسے Cloudflare Access، Authelia یا دیگر تصدیقی طریقہ کار۔",
"passwordNotMatchMsg": "دہرانے والا پاس ورڈ مماثل نہیں ہے۔",
"autoGet": "آٹو حاصل کریں",
"add": "شامل کریں",
"Edit": "ترمیم",
"applyToYAML": "YAML پر درخواست دیں۔",
"createExternalNetwork": "بنانا",
"addInternalNetwork": "شامل کریں",
"Save": "محفوظ کریں",
"Language": "زبان",
"Current User": "موجودہ صارف",
"Change Password": "پاس ورڈ تبدیل کریں",
"Current Password": "موجودہ خفیہ لفظ",
"New Password": "نیا پاس ورڈ",
"Repeat New Password": "نیا پاس ورڈ دہرائیں",
"Update Password": "پاس ورڈ اپ ڈیٹ کریں",
"Advanced": "ترقی یافتہ",
"Please use this option carefully!": "براہ کرم اس اختیار کو احتیاط سے استعمال کریں!",
"Enable Auth": "تصدیق کو فعال کریں۔",
"Disable Auth": "توثیق کو غیر فعال کریں۔",
"I understand, please disable": "میں سمجھتا ہوں، براہ کرم غیر فعال کریں۔",
"Leave": "چھوڑ دو",
"Frontend Version": "فرنٹ اینڈ ورژن",
"Check Update On GitHub": "گیتوب پر اپ ڈیٹ چیک کریں۔",
"Show update if available": "اگر دستیاب ہو تو اپ ڈیٹ دکھائیں",
"Also check beta release": "بیٹا ریلیز بھی چیک کریں",
"Remember me": "مجھے پہچانتے ہو",
"Login": "لاگ ان کریں",
"Username": "صارف نام",
"Password": "پاس ورڈ",
"Settings": "ترتیبات",
"Logout": "لاگ آوٹ",
"Lowercase only": "صرف لوئر کیس",
"Convert to Compose": "تحریر میں تبدیل کریں",
"Docker Run": "ڈاکر رن",
"active": "فعال",
"exited": "باہر نکلا",
"inactive": "غیر فعال",
"Appearance": "ظہور",
"Security": "سیکورٹی",
"About": "کے بارے میں",
"Allowed commands:": "اجازت شدہ احکامات:",
"Internal Networks": "اندرونی نیٹ ورکس",
"External Networks": "بیرونی نیٹ ورکس",
"No External Networks": "کوئی بیرونی نیٹ ورک نہیں"
}

View File

@ -1,94 +0,0 @@
{
"languageName": "简体中文",
"Create your admin account": "创建你的管理员账号",
"authIncorrectCreds": "用户名或密码错误",
"PasswordsDoNotMatch": "两次输入的密码不一致。",
"Repeat Password": "重复以确认密码",
"Create": "创建",
"signedInDisp": "当前用户: {0}",
"signedInDispDisabled": "已禁用身份验证",
"home": "主页",
"console": "终端",
"registry": "镜像仓库",
"compose": "Compose",
"addFirstStackMsg": "组合你的第一个堆栈!",
"stackName" : "堆栈名称",
"deployStack": "部署",
"deleteStack": "删除",
"stopStack": "停止",
"restartStack": "重启",
"updateStack": "更新",
"startStack": "启动",
"editStack": "编辑",
"discardStack": "放弃",
"saveStackDraft": "保存",
"notAvailableShort" : "不可用",
"deleteStackMsg": "你确定要删除这个堆栈吗?",
"stackNotManagedByDockgeMsg": "这个堆栈不由Dockge管理",
"primaryHostname": "主机名",
"general": "常规",
"container": "容器 | 容器组",
"scanFolder": "扫描堆栈文件夹",
"dockerImage": "镜像",
"restartPolicyUnlessStopped": "除非手动停止",
"restartPolicyAlways": "始终",
"restartPolicyOnFailure": "在失败时",
"restartPolicyNo": "不重启",
"environmentVariable": "环境变量 | 环境变量组",
"restartPolicy": "重启策略",
"containerName": "容器名",
"port": "端口 | 端口组",
"volume": "数据卷 | 数据卷组",
"network": "网络 | 网络组",
"dependsOn": "容器依赖 | 容器依赖关系",
"addListItem": "添加 {0}",
"deleteContainer": "删除",
"addContainer": "添加容器",
"addNetwork": "添加网络",
"disableauth.message1": "你确定要<strong>禁用身份验证</strong>吗?",
"disableauth.message2": "该选项设计用于某些场景,<strong>例如在Dockge之上接入第三方认证</strong>比如Cloudflare Access、Authelia或其他认证机制如果你不清楚这个选项的作用不要禁用验证",
"passwordNotMatchMsg": "两次输入的密码不一致。",
"autoGet": "自动获取",
"add": "添加",
"Edit": "编辑",
"applyToYAML": "应用到YAML",
"createExternalNetwork": "创建",
"addInternalNetwork": "添加",
"Save": "保存",
"Language": "语言",
"Current User": "当前用户",
"Change Password": "更换密码",
"Current Password": "当前密码",
"New Password": "新密码",
"Repeat New Password": "重复以确认新密码",
"Update Password": "更新密码",
"Advanced": "进阶",
"Please use this option carefully!": "请谨慎使用该选项!",
"Enable Auth": "启用验证",
"Disable Auth": "禁用验证",
"I understand, please disable": "我已了解风险,确认禁用",
"Leave": "离开",
"Frontend Version": "前端版本",
"Check Update On GitHub": "在GitHub上检查更新",
"Show update if available": "有更新时提醒我",
"Also check beta release": "同时检查Beta渠道更新",
"Remember me": "记住我",
"Login": "登录",
"Username": "用户名",
"Password": "密码",
"Settings": "设置",
"Logout": "登出",
"Lowercase only": "仅小写字母",
"Convert to Compose": "转换为Compose格式",
"Docker Run": "Docker启动",
"active": "已启动",
"exited": "已退出",
"inactive": "未启动",
"Appearance": "外观",
"Security": "安全",
"About": "关于",
"Allowed commands:": "允许使用的指令:",
"Internal Networks": "内部网络",
"External Networks": "外部网络",
"No External Networks": "无外部网络"
}

View File

@ -13,7 +13,6 @@ import Toast, { POSITION, useToast } from "vue-toastification";
import "xterm/lib/xterm.js"; import "xterm/lib/xterm.js";
// CSS // CSS
import "@fontsource/jetbrains-mono";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
import "xterm/css/xterm.css"; import "xterm/css/xterm.css";
import "./styles/main.scss"; import "./styles/main.scss";
@ -23,9 +22,6 @@ import socket from "./mixins/socket";
import lang from "./mixins/lang"; import lang from "./mixins/lang";
import theme from "./mixins/theme"; import theme from "./mixins/theme";
// Set Title
document.title = document.title + " - " + location.host;
const app = createApp(rootApp()); const app = createApp(rootApp());
app.use(Toast, { app.use(Toast, {

View File

@ -68,10 +68,9 @@
<h4 class="mb-3">{{ $t("general") }}</h4> <h4 class="mb-3">{{ $t("general") }}</h4>
<div class="shadow-box big-padding mb-3"> <div class="shadow-box big-padding mb-3">
<!-- Stack Name --> <!-- Stack Name -->
<div> <div class="mb-3">
<label for="name" class="form-label">{{ $t("stackName") }}</label> <label for="name" class="form-label">{{ $t("stackName") }}</label>
<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>
<div class="form-text">{{ $t("Lowercase only") }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -118,7 +117,7 @@
</div> </div>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<h4 class="mb-3">{{ stack.composeFileName }}</h4> <h4 class="mb-3">compose.yaml</h4>
<!-- YAML editor --> <!-- YAML editor -->
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}"> <div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
@ -583,10 +582,6 @@ export default {
}); });
}, },
stackNameToLowercase() {
this.stack.name = this.stack?.name?.toLowerCase();
},
} }
}; };
</script> </script>
@ -597,8 +592,6 @@ export default {
} }
.editor-box { .editor-box {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
&.edit-mode { &.edit-mode {
background-color: #2c2f38 !important; background-color: #2c2f38 !important;
} }

View File

@ -5,7 +5,7 @@
<div> <div>
<p> <p>
{{ $t("Allowed commands:") }} Allowed commands:
<template v-for="(command, index) in allowedCommandList" :key="command"> <template v-for="(command, index) in allowedCommandList" :key="command">
<code>{{ command }}</code> <code>{{ command }}</code>

View File

@ -22,12 +22,12 @@
</div> </div>
</div> </div>
<h2 class="mb-3">{{ $t("Docker Run") }}</h2> <h2 class="mb-3">Docker Run</h2>
<div class="mb-3"> <div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea> <textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</div> </div>
<button class="btn-normal btn" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button> <button class="btn-normal btn" @click="convertDockerRun">Convert to Compose</button>
</div> </div>
</transition> </transition>
<router-view ref="child" /> <router-view ref="child" />
@ -227,7 +227,5 @@ table {
.docker-run { .docker-run {
background-color: $dark-bg !important; background-color: $dark-bg !important;
border: none; border: none;
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
} }
</style> </style>

View File

@ -75,7 +75,7 @@ export default {
subMenus() { subMenus() {
return { return {
general: { general: {
title: this.$t("general"), title: this.$t("General"),
}, },
appearance: { appearance: {
title: this.$t("Appearance"), title: this.$t("Appearance"),

View File

@ -680,10 +680,6 @@ code {
} }
} }
.form-text {
color: $dark-font-color3;
}
// Vue Prism Editor bug - workaround // Vue Prism Editor bug - workaround
// https://github.com/koca/vue-prism-editor/issues/87 // https://github.com/koca/vue-prism-editor/issues/87
/* /*

View File

@ -10,7 +10,7 @@ import { POSITION } from "vue-toastification";
* *
* Generated by Trelent * Generated by Trelent
*/ */
function getTimezoneOffset(timeZone : string) { function getTimezoneOffset(timeZone) {
const now = new Date(); const now = new Date();
const tzString = now.toLocaleString("en-US", { const tzString = now.toLocaleString("en-US", {
timeZone, timeZone,
@ -124,6 +124,33 @@ export function hostNameRegexPattern(mqtt = false) {
return `${ipRegexPattern}|${hostNameRegexPattern}`; return `${ipRegexPattern}|${hostNameRegexPattern}`;
} }
/**
* Get the tag color options
* Shared between components
* @param {any} self Component
* @returns {object[]} Colour options
*/
export function colorOptions(self) {
return [
{ name: self.$t("Gray"),
color: "#4B5563" },
{ name: self.$t("Red"),
color: "#DC2626" },
{ name: self.$t("Orange"),
color: "#D97706" },
{ name: self.$t("Green"),
color: "#059669" },
{ name: self.$t("Blue"),
color: "#2563EB" },
{ name: self.$t("Indigo"),
color: "#4F46E5" },
{ name: self.$t("Purple"),
color: "#7C3AED" },
{ name: self.$t("Pink"),
color: "#DB2777" },
];
}
/** /**
* Loads the toast timeout settings from storage. * Loads the toast timeout settings from storage.
* @returns {object} The toast plugin options object. * @returns {object} The toast plugin options object.

View File

@ -1,4 +1,3 @@
/* eslint-disable */
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module "*.vue" { declare module "*.vue" {

View File

@ -1,31 +1,29 @@
{ {
"name": "dockge", "name": "dockge",
"version": "1.1.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"fmt": "eslint \"**/*.{ts,vue}\" --fix", "fmt": "eslint \"**/*.{ts,vue}\" --fix",
"lint": "eslint \"**/*.{ts,vue}\"", "lint": "eslint \"**/*.{ts,vue}\"",
"check-ts": "tsc --noEmit",
"start": "tsx ./backend/index.ts", "start": "tsx ./backend/index.ts",
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts", "dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts",
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts", "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",
"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": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:1.0.0 -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",
"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"
}, },
"dependencies": { "dependencies": {
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.11", "@homebridge/node-pty-prebuilt-multiarch": "~0.11.10",
"@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",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~6.1.0", "compare-versions": "~6.1.0",
"composerize": "~1.4.1", "composerize": "~1.4.1",
"croner": "~7.0.5", "croner": "~7.0.4",
"dayjs": "~1.11.10", "dayjs": "~1.11.10",
"express": "~4.18.2", "express": "~4.18.2",
"express-static-gzip": "~2.1.7", "express-static-gzip": "~2.1.7",
@ -34,8 +32,8 @@
"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.3",
"redbean-node": "~0.3.3", "redbean-node": "0.3.2",
"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",
@ -45,19 +43,17 @@
"yaml": "~2.3.4" "yaml": "~2.3.4"
}, },
"devDependencies": { "devDependencies": {
"@fontsource/jetbrains-mono": "^5.0.17",
"@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/bootstrap": "~5.2.8",
"@types/bootstrap": "~5.2.9",
"@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.4",
"@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.3.4",
"bootstrap": "5.3.2", "bootstrap": "5.3.2",
"bootstrap-vue-next": "~0.14.10", "bootstrap-vue-next": "~0.14.10",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
@ -68,7 +64,7 @@
"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": "~4.5.0",
"vite-plugin-compression": "~0.5.1", "vite-plugin-compression": "~0.5.1",
"vue": "~3.3.8", "vue": "~3.3.8",
"vue-eslint-parser": "~9.3.2", "vue-eslint-parser": "~9.3.2",
@ -77,7 +73,7 @@
"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", "xterm": "~5.4.0-beta.37",
"xterm-addon-web-links": "~0.9.0" "xterm-addon-web-links": "~0.9.0"
} }
} }

645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,6 @@
"module": "ESNext", "module": "ESNext",
"target": "ESNext", "target": "ESNext",
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler"
"skipLibCheck": true }
},
"include": [
"backend/**/*"
],
} }