mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-09-26 19:31:18 +08:00
ci: ko build
This commit is contained in:
30
.github/workflows/latest.workflow.yml
vendored
30
.github/workflows/latest.workflow.yml
vendored
@@ -41,27 +41,13 @@ jobs:
|
|||||||
automatic_release_tag: latest
|
automatic_release_tag: latest
|
||||||
files: |
|
files: |
|
||||||
dist/*
|
dist/*
|
||||||
|
- name: Setup ko
|
||||||
build-docker:
|
uses: ko-build/setup-ko@v0.6
|
||||||
runs-on: ubuntu-latest
|
env:
|
||||||
steps:
|
KO_DOCKER_REPO: docker.io/vaalacat/frp-panel
|
||||||
- uses: actions/checkout@v4
|
- env:
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push
|
run: |
|
||||||
uses: docker/build-push-action@v3
|
echo "${password}" | ko login docker.io --username ${username} --password-stdin
|
||||||
with:
|
ko build ./cmd/frpp --bare
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.standalone
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: vaalacat/frp-panel:latest
|
|
36
.github/workflows/tag.workflow.yml
vendored
36
.github/workflows/tag.workflow.yml
vendored
@@ -42,32 +42,14 @@ jobs:
|
|||||||
automatic_release_tag: ${{ steps.get_version.outputs.VERSION }}
|
automatic_release_tag: ${{ steps.get_version.outputs.VERSION }}
|
||||||
files: |
|
files: |
|
||||||
dist/*
|
dist/*
|
||||||
|
- name: Setup ko
|
||||||
build-docker:
|
uses: ko-build/setup-ko@v0.6
|
||||||
runs-on: ubuntu-latest
|
env:
|
||||||
steps:
|
KO_DOCKER_REPO: docker.io/vaalacat/frp-panel
|
||||||
- name: Get version
|
- name: Build image with ko
|
||||||
id: get_version
|
env:
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Get git tag history
|
|
||||||
run: git fetch -a
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push
|
run: |
|
||||||
uses: docker/build-push-action@v3
|
echo "${password}" | ko login docker.io --username ${username} --password-stdin
|
||||||
with:
|
ko build ./cmd/frpp --bare -t ${{ steps.get_version.outputs.VERSION }}
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.standalone
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: vaalacat/frp-panel:${{ steps.get_version.outputs.VERSION }}
|
|
15
.ko.yaml
Normal file
15
.ko.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
defaultBaseImage: alpine
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: frpp
|
||||||
|
dir: .
|
||||||
|
main: ./cmd/frpp
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X github.com/VaalaCat/frp-panel/conf.buildDate={{.Date}}
|
||||||
|
- -X github.com/VaalaCat/frp-panel/conf.gitCommit={{.Git.FullCommit}}
|
||||||
|
- -X github.com/VaalaCat/frp-panel/conf.gitVersion={{.Git.Tag}}
|
||||||
|
- -X github.com/VaalaCat/frp-panel/conf.gitBranch={{.Git.Branch}}
|
||||||
|
|
||||||
|
defaultPlatforms:
|
||||||
|
- all
|
3
build.sh
3
build.sh
@@ -19,6 +19,7 @@ ARCH="all"
|
|||||||
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
GIT_COMMIT="$(git rev-parse HEAD)"
|
GIT_COMMIT="$(git rev-parse HEAD)"
|
||||||
VERSION="$(git describe --tags --abbrev=0 | tr -d '\n')"
|
VERSION="$(git describe --tags --abbrev=0 | tr -d '\n')"
|
||||||
|
GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
while [[ "$#" -gt 0 ]]; do
|
while [[ "$#" -gt 0 ]]; do
|
||||||
@@ -50,7 +51,7 @@ echo "Build Date: $BUILD_DATE"
|
|||||||
echo "Git Commit: $GIT_COMMIT"
|
echo "Git Commit: $GIT_COMMIT"
|
||||||
echo "Version: $VERSION"
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
BUILD_LD_FLAGS="-X 'github.com/VaalaCat/frp-panel/conf.buildDate=${BUILD_DATE}' -X 'github.com/VaalaCat/frp-panel/conf.gitCommit=${GIT_COMMIT}' -X 'github.com/VaalaCat/frp-panel/conf.gitVersion=${VERSION}'"
|
BUILD_LD_FLAGS="-X 'github.com/VaalaCat/frp-panel/conf.buildDate=${BUILD_DATE}' -X 'github.com/VaalaCat/frp-panel/conf.gitCommit=${GIT_COMMIT}' -X 'github.com/VaalaCat/frp-panel/conf.gitVersion=${VERSION}' -X 'github.com/VaalaCat/frp-panel/conf.gitBranch=${GIT_BRANCH}'"
|
||||||
|
|
||||||
if [[ "$SKIP_FRONTEND" == "true" ]]; then
|
if [[ "$SKIP_FRONTEND" == "true" ]]; then
|
||||||
echo "Skipping frontend build"
|
echo "Skipping frontend build"
|
||||||
|
@@ -3,11 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/VaalaCat/frp-panel/conf"
|
"github.com/VaalaCat/frp-panel/conf"
|
||||||
"github.com/VaalaCat/frp-panel/logger"
|
"github.com/VaalaCat/frp-panel/logger"
|
||||||
"github.com/VaalaCat/frp-panel/utils"
|
"github.com/VaalaCat/frp-panel/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -140,6 +142,7 @@ func initCommand() {
|
|||||||
rootCmd.AddCommand(clientCmd, serverCmd, masterCmd, versionCmd,
|
rootCmd.AddCommand(clientCmd, serverCmd, masterCmd, versionCmd,
|
||||||
installServiceCmd, uninstallServiceCmd,
|
installServiceCmd, uninstallServiceCmd,
|
||||||
startServiceCmd, stopServiceCmd, restartServiceCmd)
|
startServiceCmd, stopServiceCmd, restartServiceCmd)
|
||||||
|
|
||||||
clientCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret")
|
clientCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret")
|
||||||
serverCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret")
|
serverCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret")
|
||||||
clientCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
|
clientCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
|
||||||
@@ -191,3 +194,11 @@ func patchConfig(host, secret, clientID, clientSecret, apiScheme string, rpcPort
|
|||||||
conf.Get().Master.APIHost, conf.Get().Master.APIPort,
|
conf.Get().Master.APIHost, conf.Get().Master.APIPort,
|
||||||
conf.Get().Master.APIScheme)
|
conf.Get().Master.APIScheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setMasterCommandIfNonePresent() {
|
||||||
|
cmd, _, err := rootCmd.Find(os.Args[1:])
|
||||||
|
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
|
||||||
|
args := append([]string{"master"}, os.Args[1:]...)
|
||||||
|
rootCmd.SetArgs(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -14,5 +14,6 @@ func main() {
|
|||||||
conf.InitConfig()
|
conf.InitConfig()
|
||||||
rpc.InitRPCClients()
|
rpc.InitRPCClients()
|
||||||
|
|
||||||
|
setMasterCommandIfNonePresent()
|
||||||
rootCmd.Execute()
|
rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
bizmaster "github.com/VaalaCat/frp-panel/biz/master"
|
bizmaster "github.com/VaalaCat/frp-panel/biz/master"
|
||||||
"github.com/VaalaCat/frp-panel/biz/master/auth"
|
"github.com/VaalaCat/frp-panel/biz/master/auth"
|
||||||
@@ -80,6 +81,12 @@ func initDatabase(c context.Context) {
|
|||||||
|
|
||||||
switch conf.Get().DB.Type {
|
switch conf.Get().DB.Type {
|
||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
|
if err := utils.EnsureDirectoryExists(conf.Get().DB.DSN); err != nil {
|
||||||
|
logrus.WithError(err).Warnf("ensure directory failed, data location: [%s], keep data in current directory", conf.Get().DB.DSN)
|
||||||
|
conf.Get().DB.DSN = filepath.Base(conf.Get().DB.DSN)
|
||||||
|
logrus.Infof("new data location: [%s]", conf.Get().DB.DSN)
|
||||||
|
}
|
||||||
|
|
||||||
if sqlitedb, err := gorm.Open(sqlite.Open(conf.Get().DB.DSN), &gorm.Config{}); err != nil {
|
if sqlitedb, err := gorm.Open(sqlite.Open(conf.Get().DB.DSN), &gorm.Config{}); err != nil {
|
||||||
logrus.Panic(err)
|
logrus.Panic(err)
|
||||||
} else {
|
} else {
|
||||||
|
@@ -43,7 +43,7 @@ type Config struct {
|
|||||||
} `env-prefix:"SERVER_"`
|
} `env-prefix:"SERVER_"`
|
||||||
DB struct {
|
DB struct {
|
||||||
Type string `env:"TYPE" env-default:"sqlite3" env-description:"db type, mysql or sqlite3 and so on"`
|
Type string `env:"TYPE" env-default:"sqlite3" env-description:"db type, mysql or sqlite3 and so on"`
|
||||||
DSN string `env:"DSN" env-default:"data.db" env-description:"db dsn, for sqlite is path, other is dsn, look at https://github.com/go-sql-driver/mysql#dsn-data-source-name"`
|
DSN string `env:"DSN" env-default:"/data/data.db" env-description:"db dsn, for sqlite is path, other is dsn, look at https://github.com/go-sql-driver/mysql#dsn-data-source-name"`
|
||||||
} `env-prefix:"DB_"`
|
} `env-prefix:"DB_"`
|
||||||
Client struct {
|
Client struct {
|
||||||
ID string `env:"ID" env-description:"client id"`
|
ID string `env:"ID" env-description:"client id"`
|
||||||
|
@@ -12,12 +12,14 @@ import (
|
|||||||
var (
|
var (
|
||||||
gitVersion = "dev-build"
|
gitVersion = "dev-build"
|
||||||
gitCommit = ""
|
gitCommit = ""
|
||||||
|
gitBranch = ""
|
||||||
buildDate = "1970-01-01T00:00:00Z"
|
buildDate = "1970-01-01T00:00:00Z"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VersionInfo struct {
|
type VersionInfo struct {
|
||||||
GitVersion string `json:"gitVersion" yaml:"gitVersion"`
|
GitVersion string `json:"gitVersion" yaml:"gitVersion"`
|
||||||
GitCommit string `json:"gitCommit" yaml:"gitCommit"`
|
GitCommit string `json:"gitCommit" yaml:"gitCommit"`
|
||||||
|
GitBranch string `json:"gitBranch" yaml:"gitBranch"`
|
||||||
BuildDate string `json:"buildDate" yaml:"buildDate"`
|
BuildDate string `json:"buildDate" yaml:"buildDate"`
|
||||||
GoVersion string `json:"goVersion" yaml:"goVersion"`
|
GoVersion string `json:"goVersion" yaml:"goVersion"`
|
||||||
Compiler string `json:"compiler" yaml:"compiler"`
|
Compiler string `json:"compiler" yaml:"compiler"`
|
||||||
@@ -42,6 +44,7 @@ func (v *VersionInfo) ToProto() *pb.ClientVersion {
|
|||||||
return &pb.ClientVersion{
|
return &pb.ClientVersion{
|
||||||
GitVersion: v.GitVersion,
|
GitVersion: v.GitVersion,
|
||||||
GitCommit: v.GitCommit,
|
GitCommit: v.GitCommit,
|
||||||
|
GitBranch: v.GitBranch,
|
||||||
BuildDate: v.BuildDate,
|
BuildDate: v.BuildDate,
|
||||||
GoVersion: v.GoVersion,
|
GoVersion: v.GoVersion,
|
||||||
Compiler: v.Compiler,
|
Compiler: v.Compiler,
|
||||||
@@ -53,6 +56,7 @@ func GetVersion() *VersionInfo {
|
|||||||
return &VersionInfo{
|
return &VersionInfo{
|
||||||
GitVersion: gitVersion,
|
GitVersion: gitVersion,
|
||||||
GitCommit: gitCommit,
|
GitCommit: gitCommit,
|
||||||
|
GitBranch: gitBranch,
|
||||||
BuildDate: buildDate,
|
BuildDate: buildDate,
|
||||||
GoVersion: runtime.Version(),
|
GoVersion: runtime.Version(),
|
||||||
Compiler: runtime.Compiler,
|
Compiler: runtime.Compiler,
|
||||||
|
@@ -27,6 +27,7 @@ message ClientVersion {
|
|||||||
string GoVersion = 4;
|
string GoVersion = 4;
|
||||||
string Compiler = 5;
|
string Compiler = 5;
|
||||||
string Platform = 6;
|
string Platform = 6;
|
||||||
|
string GitBranch = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetClientsStatusRequest {
|
message GetClientsStatusRequest {
|
||||||
|
@@ -176,6 +176,7 @@ type ClientVersion struct {
|
|||||||
GoVersion string `protobuf:"bytes,4,opt,name=GoVersion,proto3" json:"GoVersion,omitempty"`
|
GoVersion string `protobuf:"bytes,4,opt,name=GoVersion,proto3" json:"GoVersion,omitempty"`
|
||||||
Compiler string `protobuf:"bytes,5,opt,name=Compiler,proto3" json:"Compiler,omitempty"`
|
Compiler string `protobuf:"bytes,5,opt,name=Compiler,proto3" json:"Compiler,omitempty"`
|
||||||
Platform string `protobuf:"bytes,6,opt,name=Platform,proto3" json:"Platform,omitempty"`
|
Platform string `protobuf:"bytes,6,opt,name=Platform,proto3" json:"Platform,omitempty"`
|
||||||
|
GitBranch string `protobuf:"bytes,7,opt,name=GitBranch,proto3" json:"GitBranch,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ClientVersion) Reset() {
|
func (x *ClientVersion) Reset() {
|
||||||
@@ -250,6 +251,13 @@ func (x *ClientVersion) GetPlatform() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ClientVersion) GetGitBranch() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.GitBranch
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type GetClientsStatusRequest struct {
|
type GetClientsStatusRequest struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -502,7 +510,7 @@ var file_api_master_proto_rawDesc = []byte{
|
|||||||
0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52,
|
0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52,
|
||||||
0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x07,
|
0x10, 0x03, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x07,
|
||||||
0x0a, 0x05, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x63, 0x6f, 0x6e, 0x6e,
|
0x0a, 0x05, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x63, 0x6f, 0x6e, 0x6e,
|
||||||
0x65, 0x63, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xc1, 0x01, 0x0a, 0x0d, 0x43, 0x6c, 0x69,
|
0x65, 0x63, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x0d, 0x43, 0x6c, 0x69,
|
||||||
0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x47, 0x69,
|
0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x47, 0x69,
|
||||||
0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
|
0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
|
||||||
0x47, 0x69, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x47, 0x69,
|
0x47, 0x69, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x47, 0x69,
|
||||||
@@ -514,46 +522,48 @@ var file_api_master_proto_rawDesc = []byte{
|
|||||||
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72,
|
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72,
|
||||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72,
|
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72,
|
||||||
0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x06, 0x20, 0x01,
|
0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x06, 0x20, 0x01,
|
||||||
0x28, 0x09, 0x52, 0x08, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x6d, 0x0a, 0x17,
|
0x28, 0x09, 0x52, 0x08, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1c, 0x0a, 0x09,
|
||||||
0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
0x47, 0x69, 0x74, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e,
|
0x09, 0x47, 0x69, 0x74, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x22, 0x6d, 0x0a, 0x17, 0x47, 0x65,
|
||||||
0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63,
|
0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
|
||||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f,
|
||||||
0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
|
0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
|
0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a,
|
||||||
0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0xf5, 0x01, 0x0a, 0x18,
|
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c,
|
||||||
0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
|
0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
|
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0xf5, 0x01, 0x0a, 0x18, 0x47, 0x65,
|
||||||
0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f,
|
0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
|
||||||
0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||||
0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x4b, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e,
|
||||||
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x6d, 0x61, 0x73,
|
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||||
0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74,
|
0x88, 0x01, 0x01, 0x12, 0x4b, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02,
|
||||||
0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6c, 0x69,
|
0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x6d, 0x61, 0x73, 0x74, 0x65,
|
||||||
0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e,
|
0x72, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74,
|
||||||
0x74, 0x73, 0x1a, 0x54, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74,
|
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e,
|
||||||
0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73,
|
||||||
0x03, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
|
0x1a, 0x54, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72,
|
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
|
||||||
0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x76,
|
0x65, 0x79, 0x12, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61,
|
0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2e, 0x43,
|
||||||
0x74, 0x75, 0x73, 0x22, 0x8d, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e,
|
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c,
|
||||||
0x74, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0b,
|
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75,
|
||||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x73, 0x22, 0x8d, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43,
|
||||||
0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e,
|
0x65, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0b, 0x63, 0x6c,
|
||||||
0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70,
|
0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||||
0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02,
|
0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23,
|
0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||||
0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18,
|
0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63,
|
0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d,
|
||||||
0x72, 0x65, 0x74, 0x22, 0x63, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
|
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20,
|
||||||
0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06,
|
0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65,
|
||||||
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63,
|
0x74, 0x22, 0x63, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65,
|
||||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06,
|
0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x73, 0x74,
|
||||||
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72,
|
0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x42, 0x09, 0x0a,
|
0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74,
|
||||||
0x07, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x2e, 0x2f, 0x70,
|
0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18,
|
||||||
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f,
|
||||||
|
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x2e, 0x2f, 0x70, 0x62, 0x62,
|
||||||
|
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
18
utils/files.go
Normal file
18
utils/files.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EnsureDirectoryExists(filePath string) error {
|
||||||
|
directory := filepath.Dir(filePath)
|
||||||
|
|
||||||
|
if _, err := os.Stat(directory); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(directory, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -12,6 +12,9 @@ import { ProxyInfo } from '@/lib/pb/common'
|
|||||||
import { Button } from '../ui/button'
|
import { Button } from '../ui/button'
|
||||||
import { CheckCircle2, CircleX, RefreshCcw } from "lucide-react"
|
import { CheckCircle2, CircleX, RefreshCcw } from "lucide-react"
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import TrafficStatsCard from './stats-item'
|
||||||
|
import { Separator } from '@radix-ui/react-separator'
|
||||||
|
import { formatBytes } from '@/lib/utils'
|
||||||
|
|
||||||
export interface ClientStatsCardProps {
|
export interface ClientStatsCardProps {
|
||||||
clientID?: string
|
clientID?: string
|
||||||
@@ -19,7 +22,6 @@ export interface ClientStatsCardProps {
|
|||||||
export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defaultClientID }: ClientStatsCardProps = {}) => {
|
export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defaultClientID }: ClientStatsCardProps = {}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [clientID, setClientID] = useState<string | undefined>()
|
const [clientID, setClientID] = useState<string | undefined>()
|
||||||
const [proxyName, setProxyName] = useState<string | undefined>()
|
|
||||||
const [status, setStatus] = useState<"loading" | "success" | "error" | undefined>()
|
const [status, setStatus] = useState<"loading" | "success" | "error" | undefined>()
|
||||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
@@ -69,11 +71,6 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
|
|||||||
return Array.from(mergedMap.values());
|
return Array.from(mergedMap.values());
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeDuplicateCharacters(input: string): string {
|
|
||||||
const uniqueChars = new Set(input);
|
|
||||||
return Array.from(uniqueChars).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -88,18 +85,16 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
|
|||||||
<Label>{t('client.stats.label')}</Label>
|
<Label>{t('client.stats.label')}</Label>
|
||||||
<ClientSelector clientID={clientID} setClientID={handleClientChange} onOpenChange={() => {
|
<ClientSelector clientID={clientID} setClientID={handleClientChange} onOpenChange={() => {
|
||||||
refetchClientStats()
|
refetchClientStats()
|
||||||
setProxyName(undefined)
|
|
||||||
}} />
|
}} />
|
||||||
<Label>{t('proxy.stats.label')}</Label>
|
<Label>{t('proxy.stats.label')}</Label>
|
||||||
<ProxySelector
|
<div className="w-full grid gap-2 grid-cols-1 overflow-x-auto">
|
||||||
// @ts-ignore
|
|
||||||
proxyNames={Array.from(new Set(clientStatsList?.proxyInfos.map((proxyInfo) => proxyInfo.name).filter((value) => value !== undefined))) || []}
|
|
||||||
proxyName={proxyName}
|
|
||||||
setProxyname={setProxyName} />
|
|
||||||
<div className="w-full grid gap-4 grid-cols-1">
|
|
||||||
{clientStatsList && clientStatsList.proxyInfos.length > 0 &&
|
{clientStatsList && clientStatsList.proxyInfos.length > 0 &&
|
||||||
<ProxyStatusCard
|
clientStatsList.proxyInfos.map((proxyInfo) => {
|
||||||
proxyInfo={mergeProxyInfos(clientStatsList.proxyInfos).find((proxyInfo) => proxyInfo.name === proxyName)} />}
|
return (
|
||||||
|
<ProxyStatusCard key={proxyInfo.name} proxyInfo={proxyInfo} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
@@ -135,21 +130,23 @@ const ProxyStatusCard: React.FC<{ proxyInfo: ProxyInfo | undefined }> = ({ proxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={proxyInfo.name} className="flex flex-col space-y-4">
|
<Card key={proxyInfo.name} className="flex flex-row gap-2 p-4 w-full min-w-[900px] shadow-none justify-between">
|
||||||
<Label>{t('proxy.stats.tunnel_traffic', { name: proxyInfo.name })}</Label>
|
<Label>{t('proxy.stats.tunnel_traffic', { name: proxyInfo.name })}</Label>
|
||||||
<ProxyTrafficOverview proxyInfo={proxyInfo} />
|
<Separator orientation="vertical" />
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
|
<ProxyTrafficField label={t('traffic.today.total')} value={formatBytes(Number(proxyInfo.todayTrafficOut || BigInt(0)))} />
|
||||||
<ProxyTrafficPieChart
|
<ProxyTrafficField label={t('traffic.today.inbound')} value={formatBytes(Number(proxyInfo.todayTrafficIn || BigInt(0)))} />
|
||||||
title={t('proxy.stats.today_traffic_title')}
|
<ProxyTrafficField label={t('traffic.today.outbound')} value={formatBytes(Number(proxyInfo.todayTrafficOut || BigInt(0)))} />
|
||||||
chartLabel={t('proxy.stats.today_traffic_total')}
|
<Separator orientation="vertical" />
|
||||||
trafficIn={proxyInfo.todayTrafficIn || BigInt(0)}
|
<ProxyTrafficField label={t('traffic.history.total')} value={formatBytes(Number(proxyInfo.historyTrafficOut || BigInt(0)))} />
|
||||||
trafficOut={proxyInfo.todayTrafficOut || BigInt(0)} />
|
<ProxyTrafficField label={t('traffic.history.inbound')} value={formatBytes(Number(proxyInfo.historyTrafficIn || BigInt(0)))} />
|
||||||
<ProxyTrafficPieChart
|
<ProxyTrafficField label={t('traffic.history.outbound')} value={formatBytes(Number(proxyInfo.historyTrafficOut || BigInt(0)))} />
|
||||||
title={t('proxy.stats.history_traffic_title')}
|
</Card>
|
||||||
chartLabel={t('proxy.stats.history_traffic_total')}
|
|
||||||
trafficIn={proxyInfo.historyTrafficIn || BigInt(0)}
|
|
||||||
trafficOut={proxyInfo.historyTrafficOut || BigInt(0)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProxyTrafficField = ({ label, value }: { label: string, value: string }) => {
|
||||||
|
return <div className="flex w-fit flex-col">
|
||||||
|
<p className="text-xs text-muted-foreground text-nowrap">{label}</p>
|
||||||
|
<div className="flex items-center text-xs font-semibold text-nowrap">{value}</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
114
www/components/stats/stats-item.tsx
Normal file
114
www/components/stats/stats-item.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { ProxyInfo } from "@/lib/pb/common";
|
||||||
|
import { formatBytes } from "@/lib/utils";
|
||||||
|
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F'];
|
||||||
|
|
||||||
|
function preparePieData(inTraffic: bigint, outTraffic: bigint) {
|
||||||
|
return [
|
||||||
|
{ name: 'In', value: Number(inTraffic) },
|
||||||
|
{ name: 'Out', value: Number(outTraffic) }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTotalTraffic(inTraffic: bigint, outTraffic: bigint): bigint {
|
||||||
|
return inTraffic + outTraffic;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrafficStatsCard({ proxy }: { proxy: ProxyInfo }) {
|
||||||
|
return <Card className="p-4 hover:bg-accent/50 transition-colors">
|
||||||
|
<div className="items-center gap-4 grid grid-cols-4">
|
||||||
|
{/* Server Info */}
|
||||||
|
<div className="flex items-center gap-2 min-w-[160px]">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||||
|
<span className="font-medium">{proxy.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Today's Traffic */}
|
||||||
|
<div className="flex items-center gap-4 ">
|
||||||
|
<div className="w-16 h-16">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={preparePieData(proxy.todayTrafficIn || BigInt(0), proxy.todayTrafficOut || BigInt(0))}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={15}
|
||||||
|
outerRadius={30}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{preparePieData(proxy.todayTrafficIn || BigInt(0), proxy.todayTrafficOut || BigInt(0)).map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip formatter={(value) => formatBytes(value as number)} />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground">Today's Traffic</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">In:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(proxy.todayTrafficIn || BigInt(0)))}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">Out:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(proxy.todayTrafficOut || BigInt(0)))}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">Total:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(calculateTotalTraffic(proxy.todayTrafficIn || BigInt(0), proxy.todayTrafficOut || BigInt(0))))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* History Traffic */}
|
||||||
|
<div className="flex items-center gap-4 min-w-[300px]">
|
||||||
|
<div className="w-16 h-16">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={preparePieData(proxy.historyTrafficIn || BigInt(0), proxy.historyTrafficOut || BigInt(0))}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={15}
|
||||||
|
outerRadius={30}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{preparePieData(proxy.historyTrafficIn || BigInt(0), proxy.historyTrafficOut || BigInt(0)).map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip formatter={(value) => formatBytes(value as number)} />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground">History Traffic</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">In:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(proxy.historyTrafficIn || BigInt(0)))}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">Out:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(proxy.historyTrafficOut || BigInt(0)))}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">Total:</span>
|
||||||
|
<span className="text-sm tabular-nums">{formatBytes(Number(calculateTotalTraffic(proxy.historyTrafficIn || BigInt(0), proxy.historyTrafficOut || BigInt(0))))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
|
@@ -25,11 +25,13 @@
|
|||||||
"traffic": {
|
"traffic": {
|
||||||
"today": {
|
"today": {
|
||||||
"inbound": "Today's Inbound",
|
"inbound": "Today's Inbound",
|
||||||
"outbound": "Today's Outbound"
|
"outbound": "Today's Outbound",
|
||||||
|
"total": "Today's Total"
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"inbound": "Historical Inbound",
|
"inbound": "Historical Inbound",
|
||||||
"outbound": "Historical Outbound"
|
"outbound": "Historical Outbound",
|
||||||
|
"total": "Historical Total"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Traffic Statistics",
|
"title": "Traffic Statistics",
|
||||||
@@ -318,7 +320,7 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"label": "Tunnel Name",
|
"label": "Tunnel Name",
|
||||||
"tunnel_traffic": "Tunnel Traffic: {{name}}",
|
"tunnel_traffic": "Tunnel: {{name}}",
|
||||||
"today_traffic_title": "Today's Traffic",
|
"today_traffic_title": "Today's Traffic",
|
||||||
"today_traffic_total": "Today's Total",
|
"today_traffic_total": "Today's Total",
|
||||||
"history_traffic_title": "Historical Traffic",
|
"history_traffic_title": "Historical Traffic",
|
||||||
|
@@ -24,12 +24,14 @@
|
|||||||
},
|
},
|
||||||
"traffic": {
|
"traffic": {
|
||||||
"today": {
|
"today": {
|
||||||
"inbound": "今日入站流量",
|
"inbound": "今日入站",
|
||||||
"outbound": "今日出站流量"
|
"outbound": "今日出站",
|
||||||
|
"total": "今日总流量"
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"inbound": "历史入站流量",
|
"inbound": "历史入站",
|
||||||
"outbound": "历史出站流量"
|
"outbound": "历史出站",
|
||||||
|
"total": "历史总流量"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "流量统计",
|
"title": "流量统计",
|
||||||
@@ -318,7 +320,7 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"label": "隧道名称",
|
"label": "隧道名称",
|
||||||
"tunnel_traffic": "隧道流量:{{name}}",
|
"tunnel_traffic": "隧道:{{name}}",
|
||||||
"today_traffic_title": "今日流量",
|
"today_traffic_title": "今日流量",
|
||||||
"today_traffic_total": "今日总计",
|
"today_traffic_total": "今日总计",
|
||||||
"history_traffic_title": "历史流量",
|
"history_traffic_title": "历史流量",
|
||||||
|
@@ -94,6 +94,10 @@ export interface ClientVersion {
|
|||||||
* @generated from protobuf field: string Platform = 6 [json_name = "Platform"];
|
* @generated from protobuf field: string Platform = 6 [json_name = "Platform"];
|
||||||
*/
|
*/
|
||||||
platform: string;
|
platform: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string GitBranch = 7 [json_name = "GitBranch"];
|
||||||
|
*/
|
||||||
|
gitBranch: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf message api_master.GetClientsStatusRequest
|
* @generated from protobuf message api_master.GetClientsStatusRequest
|
||||||
@@ -254,7 +258,8 @@ class ClientVersion$Type extends MessageType<ClientVersion> {
|
|||||||
{ no: 3, name: "BuildDate", kind: "scalar", jsonName: "BuildDate", T: 9 /*ScalarType.STRING*/ },
|
{ no: 3, name: "BuildDate", kind: "scalar", jsonName: "BuildDate", T: 9 /*ScalarType.STRING*/ },
|
||||||
{ no: 4, name: "GoVersion", kind: "scalar", jsonName: "GoVersion", T: 9 /*ScalarType.STRING*/ },
|
{ no: 4, name: "GoVersion", kind: "scalar", jsonName: "GoVersion", T: 9 /*ScalarType.STRING*/ },
|
||||||
{ no: 5, name: "Compiler", kind: "scalar", jsonName: "Compiler", T: 9 /*ScalarType.STRING*/ },
|
{ no: 5, name: "Compiler", kind: "scalar", jsonName: "Compiler", T: 9 /*ScalarType.STRING*/ },
|
||||||
{ no: 6, name: "Platform", kind: "scalar", jsonName: "Platform", T: 9 /*ScalarType.STRING*/ }
|
{ no: 6, name: "Platform", kind: "scalar", jsonName: "Platform", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 7, name: "GitBranch", kind: "scalar", jsonName: "GitBranch", T: 9 /*ScalarType.STRING*/ }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
create(value?: PartialMessage<ClientVersion>): ClientVersion {
|
create(value?: PartialMessage<ClientVersion>): ClientVersion {
|
||||||
@@ -265,6 +270,7 @@ class ClientVersion$Type extends MessageType<ClientVersion> {
|
|||||||
message.goVersion = "";
|
message.goVersion = "";
|
||||||
message.compiler = "";
|
message.compiler = "";
|
||||||
message.platform = "";
|
message.platform = "";
|
||||||
|
message.gitBranch = "";
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
reflectionMergePartial<ClientVersion>(this, message, value);
|
reflectionMergePartial<ClientVersion>(this, message, value);
|
||||||
return message;
|
return message;
|
||||||
@@ -292,6 +298,9 @@ class ClientVersion$Type extends MessageType<ClientVersion> {
|
|||||||
case /* string Platform = 6 [json_name = "Platform"];*/ 6:
|
case /* string Platform = 6 [json_name = "Platform"];*/ 6:
|
||||||
message.platform = reader.string();
|
message.platform = reader.string();
|
||||||
break;
|
break;
|
||||||
|
case /* string GitBranch = 7 [json_name = "GitBranch"];*/ 7:
|
||||||
|
message.gitBranch = reader.string();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
let u = options.readUnknownField;
|
let u = options.readUnknownField;
|
||||||
if (u === "throw")
|
if (u === "throw")
|
||||||
@@ -322,6 +331,9 @@ class ClientVersion$Type extends MessageType<ClientVersion> {
|
|||||||
/* string Platform = 6 [json_name = "Platform"]; */
|
/* string Platform = 6 [json_name = "Platform"]; */
|
||||||
if (message.platform !== "")
|
if (message.platform !== "")
|
||||||
writer.tag(6, WireType.LengthDelimited).string(message.platform);
|
writer.tag(6, WireType.LengthDelimited).string(message.platform);
|
||||||
|
/* string GitBranch = 7 [json_name = "GitBranch"]; */
|
||||||
|
if (message.gitBranch !== "")
|
||||||
|
writer.tag(7, WireType.LengthDelimited).string(message.gitBranch);
|
||||||
let u = options.writeUnknownFields;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false)
|
if (u !== false)
|
||||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
Reference in New Issue
Block a user