mirror of
https://github.com/EasyTier/EasyTier.git
synced 2024-11-16 11:42:27 +08:00
introduce gui based on tauri (#52)
This commit is contained in:
parent
50e14798d6
commit
0ddcda1b31
91
.github/workflows/rust.yml
vendored
91
.github/workflows/rust.yml
vendored
|
@ -22,19 +22,25 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- TARGET: aarch64-unknown-linux-musl
|
- TARGET: aarch64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-latest
|
||||||
|
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||||
- TARGET: x86_64-unknown-linux-musl
|
- TARGET: x86_64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-latest
|
||||||
|
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||||
- TARGET: x86_64-apple-darwin
|
- TARGET: x86_64-apple-darwin
|
||||||
OS: macos-latest
|
OS: macos-latest
|
||||||
|
GUI_TARGET: x86_64-apple-darwin
|
||||||
- TARGET: aarch64-apple-darwin
|
- TARGET: aarch64-apple-darwin
|
||||||
OS: macos-latest
|
OS: macos-latest
|
||||||
|
GUI_TARGET: aarch64-apple-darwin
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
|
GUI_TARGET: x86_64-pc-windows-msvc
|
||||||
runs-on: ${{ matrix.OS }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
NAME: easytier
|
NAME: easytier
|
||||||
TARGET: ${{ matrix.TARGET }}
|
TARGET: ${{ matrix.TARGET }}
|
||||||
OS: ${{ matrix.OS }}
|
OS: ${{ matrix.OS }}
|
||||||
|
GUI_TARGET: ${{ matrix.GUI_TARGET }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup protoc
|
- name: Setup protoc
|
||||||
|
@ -42,19 +48,36 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# GitHub repo token to use to avoid rate limiter
|
# GitHub repo token to use to avoid rate limiter
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 21
|
||||||
|
cache: 'yarn'
|
||||||
|
cache-dependency-path: easytier-gui/yarn.lock
|
||||||
|
- name: Install Yarn
|
||||||
|
run: npm install -g yarn
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
uses: actions/cache@v4.0.0
|
uses: actions/cache@v4.0.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo
|
~/.cargo
|
||||||
./target
|
./target
|
||||||
key: build-cargo-registry-${{matrix.TARGET}}
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Install rust target
|
- name: Install rust target
|
||||||
run: |
|
run: |
|
||||||
# dependencies are only needed on ubuntu as that's the only place where
|
# dependencies are only needed on ubuntu as that's the only place where
|
||||||
# we make cross-compilation
|
# we make cross-compilation
|
||||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||||
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools
|
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools
|
||||||
|
# for easytier-gui
|
||||||
|
sudo apt install libwebkit2gtk-4.0-dev \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
libssl-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev
|
||||||
# curl -s musl.cc | grep mipsel
|
# curl -s musl.cc | grep mipsel
|
||||||
case $TARGET in
|
case $TARGET in
|
||||||
mipsel-unknown-linux-musl)
|
mipsel-unknown-linux-musl)
|
||||||
|
@ -87,8 +110,52 @@ jobs:
|
||||||
rustup install 1.75
|
rustup install 1.75
|
||||||
rustup default 1.75
|
rustup default 1.75
|
||||||
rustup target add $TARGET
|
rustup target add $TARGET
|
||||||
|
rustup target add $GUI_TARGET
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: cargo build --release --verbose --target $TARGET
|
run: cargo build --release --verbose --target $TARGET
|
||||||
|
- name: Install for aarch64 gui cross compile
|
||||||
|
run: |
|
||||||
|
# see https://tauri.app/v1/guides/building/linux/
|
||||||
|
if [[ $TARGET == "aarch64-unknown-linux-musl" ]]; then
|
||||||
|
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
|
sudo dpkg --add-architecture arm64
|
||||||
|
sudo apt-get update && sudo apt-get upgrade -y
|
||||||
|
sudo apt install libwebkit2gtk-4.0-dev:arm64
|
||||||
|
sudo apt install libssl-dev:arm64
|
||||||
|
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
- name: Run build GUI
|
||||||
|
run: |
|
||||||
|
cd easytier-gui
|
||||||
|
yarn install
|
||||||
|
if [[ $OS =~ ^ubuntu.*$ && ! $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||||
|
# only build deb for non-x86_64 linux
|
||||||
|
yarn tauri build -- --target $GUI_TARGET --verbose --bundles deb
|
||||||
|
else
|
||||||
|
yarn tauri build -- --target $GUI_TARGET --verbose
|
||||||
|
fi
|
||||||
- name: Compress
|
- name: Compress
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./artifacts/objects/
|
mkdir -p ./artifacts/objects/
|
||||||
|
@ -104,8 +171,21 @@ jobs:
|
||||||
TAG=$GITHUB_SHA
|
TAG=$GITHUB_SHA
|
||||||
fi
|
fi
|
||||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||||
mv ./target/$TARGET/release/easytier-gui"$SUFFIX" ./artifacts/objects/
|
|
||||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||||
|
|
||||||
|
# copy gui bundle, gui is built without specific target
|
||||||
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
|
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
||||||
|
elif [[ $OS =~ ^macos.*$ ]]; then
|
||||||
|
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||||
|
elif [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||||
|
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||||
|
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||||
|
# currently only x86 appimage is supported
|
||||||
|
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
tar -cvf ./artifacts/$NAME-$TARGET-$TAG.tar -C ./artifacts/objects/ .
|
tar -cvf ./artifacts/$NAME-$TARGET-$TAG.tar -C ./artifacts/objects/ .
|
||||||
rm -rf ./artifacts/objects/
|
rm -rf ./artifacts/objects/
|
||||||
- name: Archive artifact
|
- name: Archive artifact
|
||||||
|
@ -146,6 +226,9 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo
|
~/.cargo
|
||||||
./target
|
./target
|
||||||
key: build-cargo-registry-test
|
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: sudo -E env "PATH=$PATH" cargo test --verbose
|
run: |
|
||||||
|
sudo -E env "PATH=$PATH" cargo test --verbose
|
||||||
|
sudo chown -R $USER:$USER ./target
|
||||||
|
sudo chown -R $USER:$USER ~/.cargo
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -4,10 +4,6 @@ debug/
|
||||||
target/
|
target/
|
||||||
target-*/
|
target-*/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
@ -24,3 +20,7 @@ flamegraph.svg
|
||||||
root-target
|
root-target
|
||||||
|
|
||||||
nohup.out
|
nohup.out
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
components.d.ts
|
||||||
|
|
6262
Cargo.lock
generated
Normal file
6262
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,10 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["easytier", "easytier-gui"]
|
members = ["easytier", "easytier-gui/src-tauri"]
|
||||||
|
default-members = [ "easytier" ]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
lto = true
|
|
||||||
|
|
24
easytier-gui/.gitignore
vendored
Normal file
24
easytier-gui/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -1,48 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "easytier-gui"
|
|
||||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
|
||||||
homepage = "https://github.com/KKRainbow/EasyTier"
|
|
||||||
repository = "https://github.com/KKRainbow/EasyTier"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["kkrainbow"]
|
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
|
||||||
categories = ["network-programming"]
|
|
||||||
rust-version = "1.75"
|
|
||||||
license-file = "LICENSE"
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
easytier = { path = "../easytier" }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
anyhow = "1.0"
|
|
||||||
chrono = "0.4.37"
|
|
||||||
|
|
||||||
once_cell = "1.18.0"
|
|
||||||
dashmap = "5.5.3"
|
|
||||||
egui = { version = "0.27.2" }
|
|
||||||
egui-modal = "0.3.6"
|
|
||||||
|
|
||||||
humansize = "2.1.3"
|
|
||||||
|
|
||||||
eframe = { version = "0.27.2", features = [
|
|
||||||
"default",
|
|
||||||
"serde",
|
|
||||||
"persistence",
|
|
||||||
"wgpu"
|
|
||||||
] }
|
|
||||||
wgpu = { version = "0.19.3", features = [ "webgpu", "webgl"] }
|
|
||||||
|
|
||||||
# For image support:
|
|
||||||
egui_extras = { version = "0.27.2", features = ["default", "image"] }
|
|
||||||
|
|
||||||
env_logger = { version = "0.10", default-features = false, features = [
|
|
||||||
"auto-color",
|
|
||||||
"humantime",
|
|
||||||
] }
|
|
||||||
|
|
||||||
egui_tiles = "0.8.0"
|
|
||||||
|
|
||||||
derivative = "2.2.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
elevated-command = "1.1.2"
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Tauri + Vue 3 + TypeScript
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS
|
||||||
|
|
||||||
|
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||||
|
|
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||||
|
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||||
|
|
||||||
|
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
Binary file not shown.
14
easytier-gui/index.html
Normal file
14
easytier-gui/index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
easytier-gui/package.json
Normal file
37
easytier-gui/package.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "easytier-gui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^1",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"primeflex": "^3.3.1",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primevue": "^3.51.0",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"naive-ui": "^2.38.1",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vfonts": "^0.0.3",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vue-i18n": "^9.12.0",
|
||||||
|
"vue-tsc": "^1.8.5"
|
||||||
|
}
|
||||||
|
}
|
6
easytier-gui/postcss.config.js
Normal file
6
easytier-gui/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
7
easytier-gui/src-tauri/.gitignore
vendored
Normal file
7
easytier-gui/src-tauri/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
30
easytier-gui/src-tauri/Cargo.toml
Normal file
30
easytier-gui/src-tauri/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "easytier-gui"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "1", features = [ "process-exit", "system-tray", "shell-open"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
easytier = { path = "../../easytier" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
chrono = { version = "0.4.37", features = ["serde"] }
|
||||||
|
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
|
||||||
|
privilege = "0.3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
BIN
easytier-gui/src-tauri/Packet.dll
Normal file
BIN
easytier-gui/src-tauri/Packet.dll
Normal file
Binary file not shown.
3
easytier-gui/src-tauri/build.rs
Normal file
3
easytier-gui/src-tauri/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
easytier-gui/src-tauri/icons/icon.icns
Normal file
BIN
easytier-gui/src-tauri/icons/icon.icns
Normal file
Binary file not shown.
BIN
easytier-gui/src-tauri/icons/icon.ico
Normal file
BIN
easytier-gui/src-tauri/icons/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 186 KiB |
BIN
easytier-gui/src-tauri/icons/icon.png
Normal file
BIN
easytier-gui/src-tauri/icons/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
easytier-gui/src-tauri/icons/icon.rgba
Normal file
BIN
easytier-gui/src-tauri/icons/icon.rgba
Normal file
Binary file not shown.
|
@ -17,8 +17,9 @@ use easytier::{
|
||||||
peer::GetIpListResponse,
|
peer::GetIpListResponse,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct MyNodeInfo {
|
pub struct MyNodeInfo {
|
||||||
pub virtual_ipv4: String,
|
pub virtual_ipv4: String,
|
||||||
pub ips: GetIpListResponse,
|
pub ips: GetIpListResponse,
|
||||||
|
@ -198,10 +199,6 @@ impl EasyTierLauncher {
|
||||||
pub fn get_peers(&self) -> Vec<PeerInfo> {
|
pub fn get_peers(&self) -> Vec<PeerInfo> {
|
||||||
self.data.peers.read().unwrap().clone()
|
self.data.peers.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn running_cfg(&self) -> String {
|
|
||||||
self.running_cfg.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for EasyTierLauncher {
|
impl Drop for EasyTierLauncher {
|
353
easytier-gui/src-tauri/src/main.rs
Normal file
353
easytier-gui/src-tauri/src/main.rs
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod launcher;
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, env::current_exe, process};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use easytier::{
|
||||||
|
common::{
|
||||||
|
config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, VpnPortalConfig},
|
||||||
|
global_ctx::GlobalCtxEvent,
|
||||||
|
},
|
||||||
|
rpc::{PeerInfo, Route},
|
||||||
|
utils::{list_peer_route_pair, PeerRoutePair},
|
||||||
|
};
|
||||||
|
use launcher::{EasyTierLauncher, MyNodeInfo};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use tauri::{
|
||||||
|
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||||
|
Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
enum NetworkingMethod {
|
||||||
|
PublicServer,
|
||||||
|
Manual,
|
||||||
|
Standalone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NetworkingMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
NetworkingMethod::PublicServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
|
struct NetworkConfig {
|
||||||
|
instance_id: String,
|
||||||
|
|
||||||
|
virtual_ipv4: String,
|
||||||
|
network_name: String,
|
||||||
|
network_secret: String,
|
||||||
|
networking_method: NetworkingMethod,
|
||||||
|
|
||||||
|
public_server_url: String,
|
||||||
|
peer_urls: Vec<String>,
|
||||||
|
|
||||||
|
proxy_cidrs: Vec<String>,
|
||||||
|
|
||||||
|
enable_vpn_portal: bool,
|
||||||
|
vpn_portal_listne_port: i32,
|
||||||
|
vpn_portal_client_network_addr: String,
|
||||||
|
vpn_portal_client_network_len: i32,
|
||||||
|
|
||||||
|
advanced_settings: bool,
|
||||||
|
|
||||||
|
listener_urls: Vec<String>,
|
||||||
|
rpc_port: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkConfig {
|
||||||
|
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(
|
||||||
|
self.instance_id
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
|
||||||
|
);
|
||||||
|
cfg.set_inst_name(self.network_name.clone());
|
||||||
|
cfg.set_network_identity(NetworkIdentity {
|
||||||
|
network_name: self.network_name.clone(),
|
||||||
|
network_secret: self.network_secret.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.virtual_ipv4.len() > 0 {
|
||||||
|
cfg.set_ipv4(
|
||||||
|
self.virtual_ipv4.parse().with_context(|| {
|
||||||
|
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.networking_method {
|
||||||
|
NetworkingMethod::PublicServer => {
|
||||||
|
cfg.set_peers(vec![PeerConfig {
|
||||||
|
uri: self.public_server_url.parse().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse public server uri: {}",
|
||||||
|
self.public_server_url
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
NetworkingMethod::Manual => {
|
||||||
|
let mut peers = vec![];
|
||||||
|
for peer_url in self.peer_urls.iter() {
|
||||||
|
if peer_url.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
peers.push(PeerConfig {
|
||||||
|
uri: peer_url
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.set_peers(peers);
|
||||||
|
}
|
||||||
|
NetworkingMethod::Standalone => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut listener_urls = vec![];
|
||||||
|
for listener_url in self.listener_urls.iter() {
|
||||||
|
if listener_url.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
listener_urls.push(
|
||||||
|
listener_url
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cfg.set_listeners(listener_urls);
|
||||||
|
|
||||||
|
for n in self.proxy_cidrs.iter() {
|
||||||
|
cfg.add_proxy_cidr(
|
||||||
|
n.parse()
|
||||||
|
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.set_rpc_portal(
|
||||||
|
format!("127.0.0.1:{}", self.rpc_port)
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.enable_vpn_portal {
|
||||||
|
let cidr = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
|
||||||
|
);
|
||||||
|
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||||
|
client_cidr: cidr
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
||||||
|
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listne_port)
|
||||||
|
.parse()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse vpn portal wireguard listen port. {}",
|
||||||
|
self.vpn_portal_listne_port
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
struct NetworkInstanceRunningInfo {
|
||||||
|
my_node_info: MyNodeInfo,
|
||||||
|
events: Vec<(DateTime<Local>, GlobalCtxEvent)>,
|
||||||
|
node_info: MyNodeInfo,
|
||||||
|
routes: Vec<Route>,
|
||||||
|
peers: Vec<PeerInfo>,
|
||||||
|
peer_route_pairs: Vec<PeerRoutePair>,
|
||||||
|
running: bool,
|
||||||
|
error_msg: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetworkInstance {
|
||||||
|
config: TomlConfigLoader,
|
||||||
|
launcher: Option<EasyTierLauncher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkInstance {
|
||||||
|
fn new(cfg: NetworkConfig) -> Result<Self, anyhow::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
config: cfg.gen_config()?,
|
||||||
|
launcher: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_easytier_running(&self) -> bool {
|
||||||
|
self.launcher.is_some() && self.launcher.as_ref().unwrap().running()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_running_info(&self) -> Option<NetworkInstanceRunningInfo> {
|
||||||
|
if self.launcher.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let launcher = self.launcher.as_ref().unwrap();
|
||||||
|
|
||||||
|
let peers = launcher.get_peers();
|
||||||
|
let routes = launcher.get_routes();
|
||||||
|
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone());
|
||||||
|
|
||||||
|
Some(NetworkInstanceRunningInfo {
|
||||||
|
my_node_info: launcher.get_node_info(),
|
||||||
|
events: launcher.get_events(),
|
||||||
|
node_info: launcher.get_node_info(),
|
||||||
|
routes,
|
||||||
|
peers,
|
||||||
|
peer_route_pairs,
|
||||||
|
running: launcher.running(),
|
||||||
|
error_msg: launcher.error_msg(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
|
if self.is_easytier_running() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut launcher = EasyTierLauncher::new();
|
||||||
|
launcher.start(|| Ok(self.config.clone()));
|
||||||
|
|
||||||
|
self.launcher = Some(launcher);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||||
|
#[tauri::command]
|
||||||
|
fn parse_network_config(cfg: &str) -> Result<String, String> {
|
||||||
|
let cfg: NetworkConfig = serde_json::from_str(cfg).map_err(|e| e.to_string())?;
|
||||||
|
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
|
Ok(toml.dump())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn run_network_instance(cfg: &str) -> Result<String, String> {
|
||||||
|
let cfg: NetworkConfig = serde_json::from_str(cfg).map_err(|e| e.to_string())?;
|
||||||
|
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
|
||||||
|
return Err("instance already exists".to_string());
|
||||||
|
}
|
||||||
|
let instance_id = cfg.instance_id.clone();
|
||||||
|
|
||||||
|
let mut instance = NetworkInstance::new(cfg).map_err(|e| e.to_string())?;
|
||||||
|
instance.start().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
println!("instance {} started", instance_id);
|
||||||
|
INSTANCE_MAP.insert(instance_id, instance);
|
||||||
|
Ok("".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn retain_network_instance(instance_ids: &str) -> Result<(), String> {
|
||||||
|
let instance_ids: Vec<String> =
|
||||||
|
serde_json::from_str(instance_ids).map_err(|e| e.to_string())?;
|
||||||
|
let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k));
|
||||||
|
println!(
|
||||||
|
"instance {:?} retained",
|
||||||
|
INSTANCE_MAP
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.key().clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn collect_network_infos() -> Result<String, String> {
|
||||||
|
let mut ret = BTreeMap::new();
|
||||||
|
for instance in INSTANCE_MAP.iter() {
|
||||||
|
if let Some(info) = instance.get_running_info() {
|
||||||
|
ret.insert(instance.key().clone(), info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_string(&ret).map_err(|e| e.to_string())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_window_visibility(window: &Window) {
|
||||||
|
if window.is_visible().unwrap() {
|
||||||
|
window.hide().unwrap();
|
||||||
|
} else {
|
||||||
|
window.show().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_sudo() -> bool {
|
||||||
|
let is_elevated = privilege::user::privileged();
|
||||||
|
if !is_elevated {
|
||||||
|
let Ok(my_exe) = current_exe() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
||||||
|
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
||||||
|
}
|
||||||
|
is_elevated
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if !check_sudo() {
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
|
||||||
|
let hide = CustomMenuItem::new("hide".to_string(), "Show / Hide");
|
||||||
|
let tray_menu = SystemTrayMenu::new()
|
||||||
|
.add_item(quit)
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(hide);
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
parse_network_config,
|
||||||
|
run_network_instance,
|
||||||
|
retain_network_instance,
|
||||||
|
collect_network_infos
|
||||||
|
])
|
||||||
|
.system_tray(SystemTray::new().with_menu(tray_menu))
|
||||||
|
.on_system_tray_event(|app, event| match event {
|
||||||
|
SystemTrayEvent::DoubleClick {
|
||||||
|
position: _,
|
||||||
|
size: _,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
toggle_window_visibility(&window);
|
||||||
|
}
|
||||||
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
|
"quit" => {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
"hide" => {
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
toggle_window_visibility(&window);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_window_event(|event| match event.event() {
|
||||||
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||||
|
event.window().hide().unwrap();
|
||||||
|
api.prevent_close();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
49
easytier-gui/src-tauri/tauri.conf.json
Normal file
49
easytier-gui/src-tauri/tauri.conf.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "yarn dev",
|
||||||
|
"beforeBuildCommand": "yarn build",
|
||||||
|
"devPath": "http://localhost:1420",
|
||||||
|
"distDir": "../dist"
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "easytier-gui",
|
||||||
|
"version": "0.0.0"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false,
|
||||||
|
"shell": {
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"exit": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "easytier-gui",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"systemTray": {
|
||||||
|
"iconPath": "icons/icon.ico",
|
||||||
|
"iconAsTemplate": true
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"identifier": "com.kkrainbow.easyiter-client",
|
||||||
|
"icon": [
|
||||||
|
"icons/icon.png",
|
||||||
|
"icons/icon.rgba",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
easytier-gui/src-tauri/tauri.windows.conf.json
Normal file
16
easytier-gui/src-tauri/tauri.windows.conf.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"tauri": {
|
||||||
|
"bundle": {
|
||||||
|
"externalBin": [],
|
||||||
|
"resources": [
|
||||||
|
"./wintun.dll",
|
||||||
|
"./Packet.dll"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"type": "embedBootstrapper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
easytier-gui/src-tauri/wintun.dll
Normal file
BIN
easytier-gui/src-tauri/wintun.dll
Normal file
Binary file not shown.
254
easytier-gui/src/App.vue
Normal file
254
easytier-gui/src/App.vue
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
|
||||||
|
import Stepper from 'primevue/stepper';
|
||||||
|
import StepperPanel from 'primevue/stepperpanel';
|
||||||
|
|
||||||
|
import { useToast } from "primevue/usetoast";
|
||||||
|
|
||||||
|
import { i18n, loadLocaleFromLocalStorage, NetworkConfig, parseNetworkConfig,
|
||||||
|
useNetworkStore, runNetworkInstance, retainNetworkInstance, collectNetworkInfos,
|
||||||
|
changeLocale } from './main';
|
||||||
|
|
||||||
|
import Config from './components/Config.vue';
|
||||||
|
import Status from './components/Status.vue';
|
||||||
|
|
||||||
|
import { exit } from '@tauri-apps/api/process';
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const tomlConfig = ref("");
|
||||||
|
|
||||||
|
const items = ref([
|
||||||
|
{
|
||||||
|
label: () => i18n.global.t('show_config'),
|
||||||
|
icon: 'pi pi-file-edit',
|
||||||
|
command: async () => {
|
||||||
|
try {
|
||||||
|
const ret = await parseNetworkConfig(networkStore.curNetwork);
|
||||||
|
tomlConfig.value = ret;
|
||||||
|
} catch (e: any) {
|
||||||
|
tomlConfig.value = e;
|
||||||
|
}
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => i18n.global.t('del_cur_network'),
|
||||||
|
icon: 'pi pi-times',
|
||||||
|
command: () => {
|
||||||
|
networkStore.delCurNetwork();
|
||||||
|
},
|
||||||
|
disabled: () => networkStore.networkList.length <= 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
enum Severity {
|
||||||
|
None = "none",
|
||||||
|
Success = "success",
|
||||||
|
Info = "info",
|
||||||
|
Warn = "warn",
|
||||||
|
Error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageBarSeverity = ref(Severity.None);
|
||||||
|
const messageBarContent = ref("");
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore();
|
||||||
|
|
||||||
|
const addNewNetwork = () => {
|
||||||
|
networkStore.addNewNetwork();
|
||||||
|
networkStore.curNetwork = networkStore.lastNetwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkMenuName = (network: NetworkConfig) => {
|
||||||
|
return network.network_name + " (" + network.instance_id + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
networkStore.$subscribe(async () => {
|
||||||
|
networkStore.saveToLocalStroage();
|
||||||
|
try {
|
||||||
|
await parseNetworkConfig(networkStore.curNetwork);
|
||||||
|
messageBarSeverity.value = Severity.None;
|
||||||
|
} catch (e: any) {
|
||||||
|
messageBarContent.value = e;
|
||||||
|
messageBarSeverity.value = Severity.Error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
|
||||||
|
cb({} as MouseEvent);
|
||||||
|
networkStore.removeNetworkInstance(cfg.instance_id);
|
||||||
|
await retainNetworkInstance(networkStore.networkInstanceIds);
|
||||||
|
networkStore.addNetworkInstance(cfg.instance_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runNetworkInstance(cfg);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
toast.add({ severity: 'info', detail: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
|
||||||
|
console.log("stopNetworkCb", cfg, cb);
|
||||||
|
cb({} as MouseEvent);
|
||||||
|
networkStore.removeNetworkInstance(cfg.instance_id);
|
||||||
|
await retainNetworkInstance(networkStore.networkInstanceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNetworkInfos() {
|
||||||
|
networkStore.updateWithNetworkInfos(await collectNetworkInfos());
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervalId = 0;
|
||||||
|
onMounted(() => {
|
||||||
|
intervalId = setInterval(async () => {
|
||||||
|
await updateNetworkInfos();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
onUnmounted(() => clearInterval(intervalId))
|
||||||
|
|
||||||
|
const curNetworkHasInstance = computed(() => {
|
||||||
|
return networkStore.networkInstanceIds.includes(networkStore.curNetworkId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeStep = computed(() => {
|
||||||
|
return curNetworkHasInstance.value ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setting_menu = ref();
|
||||||
|
const setting_menu_items = ref([
|
||||||
|
{
|
||||||
|
label: () => i18n.global.t('settings'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: () => i18n.global.t('exchange_language'),
|
||||||
|
icon: 'pi pi-refresh',
|
||||||
|
command: () => {
|
||||||
|
changeLocale((i18n.global.locale.value === 'en' ? 'cn' : 'en'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => i18n.global.t('exit'),
|
||||||
|
icon: 'pi pi-times',
|
||||||
|
command: async () => {
|
||||||
|
await exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toggle_setting_menu = (event: any) => {
|
||||||
|
setting_menu.value.toggle(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
networkStore.loadFromLocalStorage();
|
||||||
|
loadLocaleFromLocalStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <n-config-provider :theme="lightTheme"> -->
|
||||||
|
<div id="root" class="flex flex-column">
|
||||||
|
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
||||||
|
<Panel>
|
||||||
|
<ScrollPanel style="width: 100%; height: 300px">
|
||||||
|
<pre>{{ tomlConfig }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
</Panel>
|
||||||
|
<Divider />
|
||||||
|
<div class="flex justify-content-end gap-2">
|
||||||
|
<Button type="button" :label="$t('close')" @click="visible = false"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Toolbar>
|
||||||
|
<template #start>
|
||||||
|
<div class="flex align-items-center gap-2">
|
||||||
|
<Button icon="pi pi-plus" class="mr-2" severity="primary" :label="$t('add_new_network')"
|
||||||
|
@click="addNewNetwork" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #center>
|
||||||
|
<div class="min-w-80 mr-20">
|
||||||
|
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList"
|
||||||
|
:optionLabel="networkMenuName" :placeholder="$t('select_network')" :highlightOnSelect="true"
|
||||||
|
:checkmark="true" class="w-full md:w-32rem" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #end>
|
||||||
|
<Button icon="pi pi-cog" class="mr-2" severity="secondary" aria-haspopup="true" @click="toggle_setting_menu"
|
||||||
|
:label="$t('settings')" aria-controls="overlay_setting_menu" />
|
||||||
|
<Menu ref="setting_menu" id="overlay_setting_menu" :model="setting_menu_items" :popup="true" />
|
||||||
|
</template>
|
||||||
|
</Toolbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stepper class="h-full overflow-y-auto" :active-step="activeStep">
|
||||||
|
<StepperPanel :header="$t('config_network')" class="w">
|
||||||
|
<template #content="{ nextCallback }">
|
||||||
|
<Config @run-network="runNetworkCb($event, nextCallback)" :instance-id="networkStore.curNetworkId"
|
||||||
|
:config-invalid="messageBarSeverity != Severity.None" />
|
||||||
|
</template>
|
||||||
|
</StepperPanel>
|
||||||
|
<StepperPanel :header="$t('running')">
|
||||||
|
<template #content="{ prevCallback }">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<Status :instance-id="networkStore.curNetworkId" />
|
||||||
|
</div>
|
||||||
|
<div class="flex pt-4 justify-content-center">
|
||||||
|
<Button label="Stop Network" severity="danger" icon="pi pi-arrow-left"
|
||||||
|
@click="stopNetworkCb(networkStore.curNetwork, prevCallback)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</StepperPanel>
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Menubar :model="items" breakpoint="300px">
|
||||||
|
</Menubar>
|
||||||
|
<InlineMessage v-if="messageBarSeverity !== Severity.None" class="absolute bottom-0 right-0" severity="error">
|
||||||
|
{{ messageBarContent }}</InlineMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-menubar .p-menuitem {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
.p-tabview-panel {
|
||||||
|
height: 100%;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
</script>
|
154
easytier-gui/src/components/Config.vue
Normal file
154
easytier-gui/src/components/Config.vue
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import InputGroup from "primevue/inputgroup";
|
||||||
|
import InputGroupAddon from "primevue/inputgroupaddon";
|
||||||
|
import { ref, defineProps, computed } from "vue";
|
||||||
|
import { i18n, useNetworkStore, NetworkingMethod } from "../main";
|
||||||
|
|
||||||
|
|
||||||
|
const networking_methods = ref([
|
||||||
|
{ value: NetworkingMethod.PublicServer, label: i18n.global.t('public_server') },
|
||||||
|
{ value: NetworkingMethod.Manual, label: i18n.global.t('manual') },
|
||||||
|
{ value: NetworkingMethod.Standalone, label: i18n.global.t('standalone') },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
configInvalid?: boolean,
|
||||||
|
instanceId?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits(["runNetwork"]);
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore();
|
||||||
|
const curNetwork = computed(() => {
|
||||||
|
if (props.instanceId) {
|
||||||
|
console.log("instanceId", props.instanceId);
|
||||||
|
const c = networkStore.networkList.find(n => n.instance_id == props.instanceId);
|
||||||
|
if (c != undefined) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkStore.curNetwork;
|
||||||
|
});
|
||||||
|
|
||||||
|
const presetPublicServers = [
|
||||||
|
"tcp://easytier.public.kkrainbow.top:11010",
|
||||||
|
];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-column h-full">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<div class="w-10/12 max-w-fit self-center ">
|
||||||
|
<Panel header="Basic Settings">
|
||||||
|
|
||||||
|
<div class="flex flex-column gap-y-2">
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="virtual_ip">{{ $t('virtual_ipv4') }}</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" aria-describedby="virtual_ipv4-help" />
|
||||||
|
<InputGroupAddon>
|
||||||
|
<span>/24</span>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="network_name">{{ $t('network_name') }}</label>
|
||||||
|
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="network_secret">{{ $t('network_secret') }}</label>
|
||||||
|
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||||
|
aria-describedby=" network_secret-help" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="nm">{{ $t('networking_method') }}</label>
|
||||||
|
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||||
|
<Dropdown v-model="curNetwork.networking_method" :options="networking_methods" optionLabel="label"
|
||||||
|
optionValue="value" placeholder="Select Method" class="" />
|
||||||
|
<Chips id="chips" v-model="curNetwork.peer_urls"
|
||||||
|
:placeholder="$t('chips_placeholder', ['tcp://8.8.8.8:11010'])" separator=" " class="grow"
|
||||||
|
v-if="curNetwork.networking_method == NetworkingMethod.Manual" />
|
||||||
|
|
||||||
|
<Dropdown :editable="true" v-model="curNetwork.public_server_url" class="grow"
|
||||||
|
:options="presetPublicServers"
|
||||||
|
v-if="curNetwork.networking_method == NetworkingMethod.PublicServer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
|
<label for="username">{{ $t('proxy_cidrs') }}</label>
|
||||||
|
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
|
||||||
|
:placeholder="$t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||||
|
<div class="flex flex-column gap-2 grow">
|
||||||
|
<label for="username">VPN Portal</label>
|
||||||
|
<div class="items-center flex flex-row gap-x-4">
|
||||||
|
<ToggleButton onIcon="pi pi-check" offIcon="pi pi-times" v-model="curNetwork.enable_vpn_portal"
|
||||||
|
:onLabel="$t('off_text')" :offLabel="$t('on_text')" />
|
||||||
|
<div class="grow" v-if="curNetwork.enable_vpn_portal">
|
||||||
|
<InputGroup>
|
||||||
|
<InputText :placeholder="$t('vpn_portal_client_network')"
|
||||||
|
v-model="curNetwork.vpn_portal_client_network_addr" />
|
||||||
|
<InputGroupAddon>
|
||||||
|
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<InputNumber :placeholder="$t('vpn_portal_listen_port')" class="" v-if="curNetwork.enable_vpn_portal"
|
||||||
|
:format="false" v-model="curNetwork.vpn_portal_listne_port" :min="0" :max="65535" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Panel :header="$t('advanced_settings')" toggleable>
|
||||||
|
<div class="flex flex-column gap-y-2">
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
|
<label for="listener_urls">{{ $t('listener_urls') }}</label>
|
||||||
|
<Chips id="listener_urls" v-model="curNetwork.listener_urls"
|
||||||
|
:placeholder="$t('chips_placeholder', ['tcp://1.1.1.1:11010'])" separator=" " class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="rpc_port">{{ $t('rpc_port') }}</label>
|
||||||
|
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
|
||||||
|
:format="false" :min="0" :max="65535" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex pt-4 justify-content-center">
|
||||||
|
<Button label="Run Network" icon="pi pi-arrow-right" iconPos="right" @click="$emit('runNetwork', curNetwork)"
|
||||||
|
:disabled="configInvalid" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
358
easytier-gui/src/components/Status.vue
Normal file
358
easytier-gui/src/components/Status.vue
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useNetworkStore } from '../main';
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instanceId?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const curNetwork = computed(() => {
|
||||||
|
if (props.instanceId) {
|
||||||
|
console.log("instanceId", props.instanceId);
|
||||||
|
const c = networkStore.networkList.find(n => n.instance_id == props.instanceId);
|
||||||
|
if (c != undefined) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkStore.curNetwork;
|
||||||
|
});
|
||||||
|
|
||||||
|
let curNetworkInst = computed(() => {
|
||||||
|
return networkStore.networkInstances.find(n => n.instance_id == curNetwork.value.instance_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
let peerRouteInfos = computed(() => {
|
||||||
|
if (curNetworkInst.value) {
|
||||||
|
return curNetworkInst.value.detail.peer_route_pairs;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
let routeCost = (info: any) => {
|
||||||
|
if (info.route) {
|
||||||
|
const cost = info.route.cost;
|
||||||
|
return cost == 1 ? "p2p" : `relay(${cost})`
|
||||||
|
}
|
||||||
|
return '?';
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveObjPath(path: string, obj = self, separator = '.') {
|
||||||
|
var properties = Array.isArray(path) ? path : path.split(separator)
|
||||||
|
return properties.reduce((prev, curr) => prev?.[curr], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
let statsCommon = (info: any, field: string) => {
|
||||||
|
if (!info.peer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let conns = info.peer.conns;
|
||||||
|
return conns.reduce((acc: number, conn: any) => {
|
||||||
|
return acc + resolveObjPath(field, conn);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
function humanFileSize(bytes: number, si = false, dp = 1) {
|
||||||
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
return bytes + ' B';
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
let u = -1;
|
||||||
|
const r = 10 ** dp;
|
||||||
|
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
|
||||||
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
let latencyMs = (info: any) => {
|
||||||
|
let lat_us_sum = statsCommon(info, 'stats.latency_us');
|
||||||
|
return lat_us_sum ? `${lat_us_sum / 1000 / info.peer.conns.length}ms` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
let txBytes = (info: any) => {
|
||||||
|
let tx = statsCommon(info, 'stats.tx_bytes');
|
||||||
|
return tx ? humanFileSize(tx) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let rxBytes = (info: any) => {
|
||||||
|
let rx = statsCommon(info, 'stats.rx_bytes');
|
||||||
|
return rx ? humanFileSize(rx) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let lossRate = (info: any) => {
|
||||||
|
let lossRate = statsCommon(info, 'loss_rate');
|
||||||
|
return lossRate != undefined ? `${Math.round(lossRate * 100)}%` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const myNodeInfo = computed(() => {
|
||||||
|
if (!curNetworkInst.value) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return curNetworkInst.value.detail?.my_node_info;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Chip {
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let myNodeInfoChips = computed(() => {
|
||||||
|
if (!curNetworkInst.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let chips: Array<Chip> = [];
|
||||||
|
let my_node_info = curNetworkInst.value.detail?.my_node_info;
|
||||||
|
if (!my_node_info) {
|
||||||
|
return chips;
|
||||||
|
}
|
||||||
|
|
||||||
|
// local ipv4s
|
||||||
|
let local_ipv4s = my_node_info.ips?.interface_ipv4s;
|
||||||
|
for (let [idx, ip] of local_ipv4s?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Local IPv4 ${idx}: ${ip}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// local ipv6s
|
||||||
|
let local_ipv6s = my_node_info.ips?.interface_ipv6s;
|
||||||
|
for (let [idx, ip] of local_ipv6s?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Local IPv6 ${idx}: ${ip}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public ip
|
||||||
|
let public_ip = my_node_info.ips?.public_ipv4;
|
||||||
|
if (public_ip) {
|
||||||
|
chips.push({
|
||||||
|
label: `Public IP: ${public_ip}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// listeners:
|
||||||
|
let listeners = my_node_info.listeners;
|
||||||
|
for (let [idx, listener] of listeners?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Listener ${idx}: ${listener}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp nat type
|
||||||
|
enum NatType {
|
||||||
|
// has NAT; but own a single public IP, port is not changed
|
||||||
|
Unknown = 0,
|
||||||
|
OpenInternet = 1,
|
||||||
|
NoPAT = 2,
|
||||||
|
FullCone = 3,
|
||||||
|
Restricted = 4,
|
||||||
|
PortRestricted = 5,
|
||||||
|
Symmetric = 6,
|
||||||
|
SymUdpFirewall = 7,
|
||||||
|
};
|
||||||
|
let udpNatType: NatType = my_node_info.stun_info?.udp_nat_type;
|
||||||
|
if (udpNatType != undefined) {
|
||||||
|
let udpNatTypeStrMap = {
|
||||||
|
[NatType.Unknown]: 'Unknown',
|
||||||
|
[NatType.OpenInternet]: 'Open Internet',
|
||||||
|
[NatType.NoPAT]: 'No PAT',
|
||||||
|
[NatType.FullCone]: 'Full Cone',
|
||||||
|
[NatType.Restricted]: 'Restricted',
|
||||||
|
[NatType.PortRestricted]: 'Port Restricted',
|
||||||
|
[NatType.Symmetric]: 'Symmetric',
|
||||||
|
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||||
|
};
|
||||||
|
|
||||||
|
chips.push({
|
||||||
|
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return chips;
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalSumCommon = (field: string) => {
|
||||||
|
let sum = 0;
|
||||||
|
if (!peerRouteInfos.value) {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
for (let info of peerRouteInfos.value) {
|
||||||
|
let tx = statsCommon(info, field);
|
||||||
|
if (tx) {
|
||||||
|
sum += tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
};
|
||||||
|
|
||||||
|
const txGlobalSum = () => {
|
||||||
|
return globalSumCommon('stats.tx_bytes');
|
||||||
|
};
|
||||||
|
|
||||||
|
const rxGlobalSum = () => {
|
||||||
|
return globalSumCommon('stats.rx_bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const peerCount = computed(() => {
|
||||||
|
if (!peerRouteInfos.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return peerRouteInfos.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// calculate tx/rx rate every 2 seconds
|
||||||
|
let rateIntervalId = 0;
|
||||||
|
let rateInterval = 2000;
|
||||||
|
let prevTxSum = 0;
|
||||||
|
let prevRxSum = 0;
|
||||||
|
let txRate = ref('0');
|
||||||
|
let rxRate = ref('0');
|
||||||
|
onMounted(() => {
|
||||||
|
rateIntervalId = setInterval(() => {
|
||||||
|
let curTxSum = txGlobalSum();
|
||||||
|
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000));
|
||||||
|
prevTxSum = curTxSum;
|
||||||
|
|
||||||
|
let curRxSum = rxGlobalSum();
|
||||||
|
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000));
|
||||||
|
prevRxSum = curRxSum;
|
||||||
|
}, rateInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(rateIntervalId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const dialogContent = ref('');
|
||||||
|
|
||||||
|
const showVpnPortalConfig = () => {
|
||||||
|
let my_node_info = myNodeInfo.value;
|
||||||
|
if (!my_node_info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = "https://www.wireguardconfig.com/qrcode";
|
||||||
|
dialogContent.value = `${my_node_info.vpn_portal_cfg}\n\n # can generate QR code: ${url}`;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showEventLogs = () => {
|
||||||
|
let detail = curNetworkInst.value?.detail;
|
||||||
|
if (!detail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogContent.value = detail.events;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Dialog v-model:visible="dialogVisible" modal header="Dialog" :style="{ width: '70%' }">
|
||||||
|
<Panel>
|
||||||
|
<ScrollPanel style="width: 100%; height: 400px">
|
||||||
|
<pre>{{ dialogContent }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
</Panel>
|
||||||
|
<Divider />
|
||||||
|
<div class="flex justify-content-end gap-2">
|
||||||
|
<Button type="button" label="Close" @click="dialogVisible = false"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Card v-if="curNetworkInst?.error_msg">
|
||||||
|
<template #title>Run Network Error</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-column gap-y-5">
|
||||||
|
<div class="text-red-500">
|
||||||
|
{{ curNetworkInst.error_msg }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card v-if="!curNetworkInst?.error_msg">
|
||||||
|
<template #title>{{ $t('my_node_info') }}</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex w-full flex-column gap-y-5">
|
||||||
|
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||||
|
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid green">
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ $t('peer_count') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-5xl mt-1">{{ peerCount }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid purple">
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ $t('upload') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xl mt-2">{{ txRate }}/s</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid fuchsia">
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ $t('download') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xl mt-2">{{ rxRate }}/s</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row align-items-center flex-wrap w-full">
|
||||||
|
<Chip v-for="chip in myNodeInfoChips" :label="chip.label" :icon="chip.icon" class="mr-2 mt-2">
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-0 flex flex-row justify-center gap-x-5 text-sm" v-if="myNodeInfo">
|
||||||
|
<Button severity="info" :label="$t('show_vpn_portal_config')" @click="showVpnPortalConfig" />
|
||||||
|
<Button severity="info" :label="$t('show_event_log')" @click="showEventLogs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Card v-if="!curNetworkInst?.error_msg">
|
||||||
|
<template #title>{{ $t('peer_info') }}</template>
|
||||||
|
<template #content>
|
||||||
|
<DataTable :value="peerRouteInfos" tableStyle="min-width: 50rem">
|
||||||
|
<Column field="route.ipv4_addr" :header="$t('virtual_ipv4')"></Column>
|
||||||
|
<Column field="route.hostname" :header="$t('hostname')"></Column>
|
||||||
|
<Column :field="routeCost" :header="$t('route_cost')"></Column>
|
||||||
|
<Column :field="latencyMs" :header="$t('latency')"></Column>
|
||||||
|
<Column :field="txBytes" :header="$t('upload_bytes')"></Column>
|
||||||
|
<Column :field="rxBytes" :header="$t('download_bytes')"></Column>
|
||||||
|
<Column :field="lossRate" :header="$t('loss_rate')"></Column>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
File diff suppressed because it is too large
Load Diff
353
easytier-gui/src/main.ts
Normal file
353
easytier-gui/src/main.ts
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
import "./styles.css";
|
||||||
|
import "primevue/resources/themes/aura-light-green/theme.css";
|
||||||
|
import "primeicons/primeicons.css";
|
||||||
|
import "primeflex/primeflex.css";
|
||||||
|
|
||||||
|
import { createPinia, defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import PrimeVue from 'primevue/config';
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import ToastService from 'primevue/toastservice';
|
||||||
|
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
export enum NetworkingMethod {
|
||||||
|
PublicServer = "PublicServer",
|
||||||
|
Manual = "Manual",
|
||||||
|
Standalone = "Standalone",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkConfig {
|
||||||
|
instance_id: string,
|
||||||
|
|
||||||
|
virtual_ipv4: string
|
||||||
|
network_name: string
|
||||||
|
network_secret: string
|
||||||
|
|
||||||
|
networking_method: NetworkingMethod,
|
||||||
|
|
||||||
|
public_server_url: string,
|
||||||
|
peer_urls: Array<string>,
|
||||||
|
|
||||||
|
proxy_cidrs: Array<string>,
|
||||||
|
|
||||||
|
enable_vpn_portal: boolean,
|
||||||
|
vpn_portal_listne_port: number,
|
||||||
|
vpn_portal_client_network_addr: string,
|
||||||
|
vpn_portal_client_network_len: number,
|
||||||
|
|
||||||
|
advanced_settings: boolean,
|
||||||
|
|
||||||
|
listener_urls: Array<string>,
|
||||||
|
rpc_port: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
function default_network(): NetworkConfig {
|
||||||
|
return {
|
||||||
|
instance_id: uuidv4(),
|
||||||
|
|
||||||
|
virtual_ipv4: "",
|
||||||
|
network_name: "default",
|
||||||
|
network_secret: "",
|
||||||
|
|
||||||
|
networking_method: NetworkingMethod.PublicServer,
|
||||||
|
|
||||||
|
public_server_url: "tcp://easytier.public.kkrainbow.top:11010",
|
||||||
|
peer_urls: [],
|
||||||
|
|
||||||
|
proxy_cidrs: [],
|
||||||
|
|
||||||
|
enable_vpn_portal: false,
|
||||||
|
vpn_portal_listne_port: 22022,
|
||||||
|
vpn_portal_client_network_addr: "",
|
||||||
|
vpn_portal_client_network_len: 24,
|
||||||
|
|
||||||
|
advanced_settings: false,
|
||||||
|
|
||||||
|
listener_urls: [
|
||||||
|
"tcp://0.0.0.0:11010",
|
||||||
|
"udp://0.0.0.0:11010",
|
||||||
|
"wg://0.0.0.0:11011",
|
||||||
|
],
|
||||||
|
rpc_port: 15888,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInstance {
|
||||||
|
instance_id: string,
|
||||||
|
|
||||||
|
running: boolean,
|
||||||
|
error_msg: string,
|
||||||
|
|
||||||
|
detail: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNetworkStore = defineStore('network', {
|
||||||
|
state: () => {
|
||||||
|
const networkList = [default_network()];
|
||||||
|
return {
|
||||||
|
// for initially empty lists
|
||||||
|
networkList: networkList as NetworkConfig[],
|
||||||
|
// for data that is not yet loaded
|
||||||
|
curNetwork: networkList[0],
|
||||||
|
|
||||||
|
// uuid -> instance
|
||||||
|
instances: {} as Record<string, NetworkInstance>,
|
||||||
|
|
||||||
|
networkInfos: {} as Record<string, any>,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
lastNetwork(): NetworkConfig {
|
||||||
|
return this.networkList[this.networkList.length - 1];
|
||||||
|
},
|
||||||
|
|
||||||
|
curNetworkId(): string {
|
||||||
|
return this.curNetwork.instance_id;
|
||||||
|
},
|
||||||
|
|
||||||
|
networkInstances(): Array<NetworkInstance> {
|
||||||
|
return Object.values(this.instances);
|
||||||
|
},
|
||||||
|
|
||||||
|
networkInstanceIds(): Array<string> {
|
||||||
|
return Object.keys(this.instances);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
addNewNetwork() {
|
||||||
|
this.networkList.push(default_network());
|
||||||
|
},
|
||||||
|
|
||||||
|
delCurNetwork() {
|
||||||
|
const curNetworkIdx = this.networkList.indexOf(this.curNetwork);
|
||||||
|
this.networkList.splice(curNetworkIdx, 1);
|
||||||
|
const nextCurNetworkIdx = Math.min(curNetworkIdx, this.networkList.length - 1);
|
||||||
|
this.curNetwork = this.networkList[nextCurNetworkIdx];
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNetworkInstance(instanceId: string) {
|
||||||
|
delete this.instances[instanceId];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNetworkInstance(instanceId: string) {
|
||||||
|
this.instances[instanceId] = {
|
||||||
|
instance_id: instanceId,
|
||||||
|
running: false,
|
||||||
|
error_msg: "",
|
||||||
|
detail: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWithNetworkInfos(networkInfos: Record<string, any>) {
|
||||||
|
this.networkInfos = networkInfos;
|
||||||
|
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||||
|
if (this.instances[instanceId] === undefined) {
|
||||||
|
this.addNetworkInstance(instanceId);
|
||||||
|
}
|
||||||
|
this.instances[instanceId].running = info["running"];
|
||||||
|
this.instances[instanceId].error_msg = info["error_msg"];
|
||||||
|
this.instances[instanceId].detail = info;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFromLocalStorage() {
|
||||||
|
const networkList = JSON.parse(localStorage.getItem("networkList") || '[]');
|
||||||
|
let result = [];
|
||||||
|
for (const cfg of networkList) {
|
||||||
|
result.push({
|
||||||
|
...default_network(),
|
||||||
|
...cfg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (result.length === 0) {
|
||||||
|
result.push(default_network());
|
||||||
|
}
|
||||||
|
this.networkList = result;
|
||||||
|
this.curNetwork = this.networkList[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
saveToLocalStroage() {
|
||||||
|
localStorage.setItem("networkList", JSON.stringify(this.networkList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function parseNetworkConfig(cfg: NetworkConfig): Promise<string> {
|
||||||
|
const ret: string = await invoke("parse_network_config", { cfg: JSON.stringify(cfg) });
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runNetworkInstance(cfg: NetworkConfig) {
|
||||||
|
const ret: string = await invoke("run_network_instance", { cfg: JSON.stringify(cfg) });
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retainNetworkInstance(instanceIds: Array<string>) {
|
||||||
|
const ret: string = await invoke("retain_network_instance", { instanceIds: JSON.stringify(instanceIds) });
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectNetworkInfos() {
|
||||||
|
const ret: string = await invoke("collect_network_infos", {});
|
||||||
|
return JSON.parse(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
"network": "Network",
|
||||||
|
"networking_method": "Networking Method",
|
||||||
|
"public_server": "Public Server",
|
||||||
|
"manual": "Manual",
|
||||||
|
"standalone": "Standalone",
|
||||||
|
"virtual_ipv4": "Virtual IPv4",
|
||||||
|
"network_name": "Network Name",
|
||||||
|
"network_secret": "Network Secret",
|
||||||
|
"public_server_url": "Public Server URL",
|
||||||
|
"peer_urls": "Peer URLs",
|
||||||
|
"proxy_cidrs": "Subnet Proxy CIDRs",
|
||||||
|
"enable_vpn_portal": "Enable VPN Portal",
|
||||||
|
"vpn_portal_listen_port": "VPN Portal Listen Port",
|
||||||
|
"vpn_portal_client_network": "Client Sub Network",
|
||||||
|
"advanced_settings": "Advanced Settings",
|
||||||
|
"listener_urls": "Listener URLs",
|
||||||
|
"rpc_port": "RPC Port",
|
||||||
|
"config_network": "Config Network",
|
||||||
|
"running": "Running",
|
||||||
|
"error_msg": "Error Message",
|
||||||
|
"detail": "Detail",
|
||||||
|
"add_new_network": "Add New Network",
|
||||||
|
"del_cur_network": "Delete Current Network",
|
||||||
|
"select_network": "Select Network",
|
||||||
|
"network_instances": "Network Instances",
|
||||||
|
"instance_id": "Instance ID",
|
||||||
|
"network_infos": "Network Infos",
|
||||||
|
"parse_network_config": "Parse Network Config",
|
||||||
|
"run_network_instance": "Run Network Instance",
|
||||||
|
"retain_network_instance": "Retain Network Instance",
|
||||||
|
"collect_network_infos": "Collect Network Infos",
|
||||||
|
"settings": "Settings",
|
||||||
|
"exchange_language": "切换中文",
|
||||||
|
"exit": "Exit",
|
||||||
|
|
||||||
|
"chips_placeholder": "e.g: {0}, press Enter to add",
|
||||||
|
"off_text": "Press to disable",
|
||||||
|
"on_text": "Press to enable",
|
||||||
|
|
||||||
|
"show_config": "Show Config",
|
||||||
|
"close": "Close",
|
||||||
|
|
||||||
|
"my_node_info": "My Node Info",
|
||||||
|
"peer_count": "Connected",
|
||||||
|
"upload": "Upload",
|
||||||
|
"download": "Download",
|
||||||
|
"show_vpn_portal_config": "Show VPN Portal Config",
|
||||||
|
"show_event_log": "Show Event Log",
|
||||||
|
"peer_info": "Peer Info",
|
||||||
|
"route_cost": "Route Cost",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"latency": "Latency",
|
||||||
|
"upload_bytes": "Upload",
|
||||||
|
"download_bytes": "Download",
|
||||||
|
"loss_rate": "Loss Rate",
|
||||||
|
},
|
||||||
|
cn: {
|
||||||
|
"network": "网络",
|
||||||
|
"networking_method": "网络方式",
|
||||||
|
"public_server": "公共服务器",
|
||||||
|
"manual": "手动",
|
||||||
|
"standalone": "独立",
|
||||||
|
"virtual_ipv4": "虚拟IPv4地址",
|
||||||
|
"network_name": "网络名称",
|
||||||
|
"network_secret": "网络密码",
|
||||||
|
"public_server_url": "公共服务器地址",
|
||||||
|
"peer_urls": "对等节点地址",
|
||||||
|
"proxy_cidrs": "子网代理CIDR",
|
||||||
|
"enable_vpn_portal": "启用VPN门户",
|
||||||
|
"vpn_portal_listen_port": "监听端口",
|
||||||
|
"vpn_portal_client_network": "客户端子网",
|
||||||
|
"advanced_settings": "高级设置",
|
||||||
|
"listener_urls": "监听地址",
|
||||||
|
"rpc_port": "RPC端口",
|
||||||
|
"config_network": "配置网络",
|
||||||
|
"running": "运行中",
|
||||||
|
"error_msg": "错误信息",
|
||||||
|
"detail": "详情",
|
||||||
|
"add_new_network": "添加新网络",
|
||||||
|
"del_cur_network": "删除当前网络",
|
||||||
|
"select_network": "选择网络",
|
||||||
|
"network_instances": "网络实例",
|
||||||
|
"instance_id": "实例ID",
|
||||||
|
"network_infos": "网络信息",
|
||||||
|
"parse_network_config": "解析网络配置",
|
||||||
|
"run_network_instance": "运行网络实例",
|
||||||
|
"retain_network_instance": "保留网络实例",
|
||||||
|
"collect_network_infos": "收集网络信息",
|
||||||
|
"settings": "设置",
|
||||||
|
"exchange_language": "Switch to English",
|
||||||
|
"exit": "退出",
|
||||||
|
"chips_placeholder": "例如: {0}, 按回车添加",
|
||||||
|
"off_text": "点击关闭",
|
||||||
|
"on_text": "点击开启",
|
||||||
|
"show_config": "显示配置",
|
||||||
|
"close": "关闭",
|
||||||
|
|
||||||
|
"my_node_info": "当前节点信息",
|
||||||
|
"peer_count": "已连接",
|
||||||
|
"upload": "上传",
|
||||||
|
"download": "下载",
|
||||||
|
"show_vpn_portal_config": "显示VPN门户配置",
|
||||||
|
"show_event_log": "显示事件日志",
|
||||||
|
"peer_info": "节点信息",
|
||||||
|
"hostname": "主机名",
|
||||||
|
"route_cost": "路由",
|
||||||
|
"latency": "延迟",
|
||||||
|
"upload_bytes": "上传",
|
||||||
|
"download_bytes": "下载",
|
||||||
|
"loss_rate": "丢包率",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLocaleToLocalStorage(locale: string) {
|
||||||
|
localStorage.setItem("locale", locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadLocaleFromLocalStorage(): string {
|
||||||
|
return localStorage.getItem("locale") || "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en', // set locale
|
||||||
|
fallbackLocale: 'cn', // set fallback locale
|
||||||
|
messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function changeLocale(locale: 'en' | 'cn') {
|
||||||
|
i18n.global.locale.value = locale;
|
||||||
|
saveLocaleToLocalStorage(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(i18n, { useScope: 'global' })
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(PrimeVue);
|
||||||
|
app.use(ToastService);
|
||||||
|
app.mount("#app");
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: "/", component: App }]
|
||||||
|
});
|
33
easytier-gui/src/styles.css
Normal file
33
easytier-gui/src/styles.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
@layer tailwind-base, primevue, tailwind-utilities;
|
||||||
|
|
||||||
|
@layer tailwind-base {
|
||||||
|
@tailwind base;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer tailwind-utilities {
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #0f0f0f;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
#[derive(Default)]
|
|
||||||
pub struct TextListOption {
|
|
||||||
pub hint: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text_list_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
id: &str,
|
|
||||||
texts: &mut Vec<String>,
|
|
||||||
option: Option<TextListOption>,
|
|
||||||
) {
|
|
||||||
let option = option.unwrap_or_default();
|
|
||||||
// convert text vec to (index, text) vec
|
|
||||||
let mut add_new_item = false;
|
|
||||||
let mut remove_idxs = vec![];
|
|
||||||
|
|
||||||
egui::Grid::new(id).max_col_width(200.0).show(ui, |ui| {
|
|
||||||
for i in 0..texts.len() {
|
|
||||||
egui::TextEdit::singleline(&mut texts[i])
|
|
||||||
.hint_text(&option.hint)
|
|
||||||
.show(ui);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("➖").clicked() {
|
|
||||||
remove_idxs.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == texts.len() - 1 {
|
|
||||||
if ui.button("➕").clicked() {
|
|
||||||
add_new_item = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
|
|
||||||
if texts.len() == 0 {
|
|
||||||
if ui.button("➕").clicked() {
|
|
||||||
add_new_item = true;
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let new_texts = texts
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(i, _)| !remove_idxs.contains(i))
|
|
||||||
.map(|(_, t)| t.clone())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
*texts = new_texts;
|
|
||||||
|
|
||||||
if add_new_item && texts.last().map(|t| !t.is_empty()).unwrap_or(true) {
|
|
||||||
texts.push("".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
//! Source code example of how to create your own widget.
|
|
||||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
|
||||||
|
|
||||||
/// iOS-style toggle switch:
|
|
||||||
///
|
|
||||||
/// ``` text
|
|
||||||
/// _____________
|
|
||||||
/// / /.....\
|
|
||||||
/// | |.......|
|
|
||||||
/// \_______\_____/
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// ``` ignore
|
|
||||||
/// toggle_ui(ui, &mut my_bool);
|
|
||||||
/// ```
|
|
||||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|
||||||
// Widget code can be broken up in four steps:
|
|
||||||
// 1. Decide a size for the widget
|
|
||||||
// 2. Allocate space for it
|
|
||||||
// 3. Handle interactions with the widget (if any)
|
|
||||||
// 4. Paint the widget
|
|
||||||
|
|
||||||
// 1. Deciding widget size:
|
|
||||||
// You can query the `ui` how much space is available,
|
|
||||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
|
||||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
|
||||||
|
|
||||||
// 2. Allocating space:
|
|
||||||
// This is where we get a region of the screen assigned.
|
|
||||||
// We also tell the Ui to sense clicks in the allocated region.
|
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
|
|
||||||
// 3. Interact: Time to check for clicks!
|
|
||||||
if response.clicked() {
|
|
||||||
*on = !*on;
|
|
||||||
response.mark_changed(); // report back that the value changed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach some meta-data to the response which can be used by screen readers:
|
|
||||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
|
||||||
|
|
||||||
// 4. Paint!
|
|
||||||
// Make sure we need to paint:
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
// Let's ask for a simple animation from egui.
|
|
||||||
// egui keeps track of changes in the boolean associated with the id and
|
|
||||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
|
||||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
|
||||||
// We will follow the current style by asking
|
|
||||||
// "how should something that is being interacted with be painted?".
|
|
||||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
|
||||||
let visuals = ui.style().interact_selectable(&response, *on);
|
|
||||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
|
||||||
let rect = rect.expand(visuals.expansion);
|
|
||||||
let radius = 0.5 * rect.height();
|
|
||||||
ui.painter()
|
|
||||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
|
||||||
// Paint the circle, animating it from left to right with `how_on`:
|
|
||||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
|
||||||
let center = egui::pos2(circle_x, rect.center().y);
|
|
||||||
ui.painter()
|
|
||||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All done! Return the interaction response so the user can check what happened
|
|
||||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Here is the same code again, but a bit more compact:
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|
||||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
if response.clicked() {
|
|
||||||
*on = !*on;
|
|
||||||
response.mark_changed();
|
|
||||||
}
|
|
||||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
|
||||||
let visuals = ui.style().interact_selectable(&response, *on);
|
|
||||||
let rect = rect.expand(visuals.expansion);
|
|
||||||
let radius = 0.5 * rect.height();
|
|
||||||
ui.painter()
|
|
||||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
|
||||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
|
||||||
let center = egui::pos2(circle_x, rect.center().y);
|
|
||||||
ui.painter()
|
|
||||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
|
||||||
/// iOS-style toggle switch.
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// ``` ignore
|
|
||||||
/// ui.add(toggle(&mut my_bool));
|
|
||||||
/// ```
|
|
||||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
|
||||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
|
||||||
}
|
|
7
easytier-gui/src/vite-env.d.ts
vendored
Normal file
7
easytier-gui/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
12
easytier-gui/tailwind.config.js
Normal file
12
easytier-gui/tailwind.config.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
25
easytier-gui/tsconfig.json
Normal file
25
easytier-gui/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
easytier-gui/tsconfig.node.json
Normal file
10
easytier-gui/tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
31
easytier-gui/vite.config.ts
Normal file
31
easytier-gui/vite.config.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { PrimeVueResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
Components({
|
||||||
|
dts: true,
|
||||||
|
resolvers: [
|
||||||
|
PrimeVueResolver()
|
||||||
|
]
|
||||||
|
})],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
watch: {
|
||||||
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
1663
easytier-gui/yarn.lock
Normal file
1663
easytier-gui/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[auto_impl::auto_impl(Box, &)]
|
#[auto_impl::auto_impl(Box, &)]
|
||||||
pub trait ConfigLoader: Send + Sync {
|
pub trait ConfigLoader: Send + Sync {
|
||||||
fn get_id(&self) -> uuid::Uuid;
|
fn get_id(&self) -> uuid::Uuid;
|
||||||
|
fn set_id(&self, id: uuid::Uuid);
|
||||||
|
|
||||||
fn get_inst_name(&self) -> String;
|
fn get_inst_name(&self) -> String;
|
||||||
fn set_inst_name(&self, name: String);
|
fn set_inst_name(&self, name: String);
|
||||||
|
@ -111,7 +112,7 @@ pub struct Flags {
|
||||||
struct Config {
|
struct Config {
|
||||||
netns: Option<String>,
|
netns: Option<String>,
|
||||||
instance_name: Option<String>,
|
instance_name: Option<String>,
|
||||||
instance_id: Option<String>,
|
instance_id: Option<uuid::Uuid>,
|
||||||
ipv4: Option<String>,
|
ipv4: Option<String>,
|
||||||
network_identity: Option<NetworkIdentity>,
|
network_identity: Option<NetworkIdentity>,
|
||||||
listeners: Option<Vec<url::Url>>,
|
listeners: Option<Vec<url::Url>>,
|
||||||
|
@ -246,21 +247,17 @@ impl ConfigLoader for TomlConfigLoader {
|
||||||
let mut locked_config = self.config.lock().unwrap();
|
let mut locked_config = self.config.lock().unwrap();
|
||||||
if locked_config.instance_id.is_none() {
|
if locked_config.instance_id.is_none() {
|
||||||
let id = uuid::Uuid::new_v4();
|
let id = uuid::Uuid::new_v4();
|
||||||
locked_config.instance_id = Some(id.to_string());
|
locked_config.instance_id = Some(id);
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
uuid::Uuid::parse_str(locked_config.instance_id.as_ref().unwrap())
|
locked_config.instance_id.as_ref().unwrap().clone()
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse instance id as uuid: {}, you can use this id: {}",
|
|
||||||
locked_config.instance_id.as_ref().unwrap(),
|
|
||||||
uuid::Uuid::new_v4()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_id(&self, id: uuid::Uuid) {
|
||||||
|
self.config.lock().unwrap().instance_id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_network_identity(&self) -> NetworkIdentity {
|
fn get_network_identity(&self) -> NetworkIdentity {
|
||||||
self.config
|
self.config
|
||||||
.lock()
|
.lock()
|
||||||
|
|
|
@ -13,7 +13,7 @@ use super::{
|
||||||
|
|
||||||
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
|
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum GlobalCtxEvent {
|
pub enum GlobalCtxEvent {
|
||||||
TunDeviceReady(String),
|
TunDeviceReady(String),
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::rpc::cli::{NatType, PeerInfo, Route};
|
use crate::rpc::cli::{NatType, PeerInfo, Route};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PeerRoutePair {
|
pub struct PeerRoutePair {
|
||||||
pub route: Route,
|
pub route: Route,
|
||||||
pub peer: Option<PeerInfo>,
|
pub peer: Option<PeerInfo>,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user